【项目实训】智能OJ平台(三):RAG检索增强模块实现
写在前面:
上一篇博客我完成了多场景 Prompt 模板的设计,针对 WA、CE、TLE 等不同判题结果给出了不同的提问方式。但我们都知道,ai有时后的回答会奇奇怪怪的,比如讲两数之和时,它可能自己编一个不存在的解法。这是因为大模型的知识来自训练数据,不一定包含我们题目库里的特定内容。那如果我们希望解决这个问题,那就需要构建一个知识库,让大模型给我们生成辅导讲解的时候可以进行参考,让ai尽量不要乱讲。所以这一篇,我来做 RAG(检索增强生成),让ai先查资料再回答。
一、本篇目标
对应任务书指标:
-
建设算法知识库,整理不少于 100 条知识片段
-
知识内容覆盖不少于 10 个常见算法主题
建一个存放算法知识的地方,后续让 AI 先查这里再回答问题。
需要做的三件事:
-
建一个知识库(存算法知识点和题解)
-
用户提问时,先从知识库里找相关内容
-
把找到的内容和问题一起发给 AI,让 AI 基于这些资料回答
二、为什么需要知识库?
直接让 AI 回答有两个问题:
| 问题 | 说明 |
|---|---|
| 幻觉 | AI 可能编造不存在的内容 |
| 不可追溯 | 不知道它的依据是什么 |
有了知识库,AI 的回答就有了“参考文献”,更可靠,也更容易追溯
出了问题,我们也可以看看参考了什么资料,也可以通过调整资料的内容来提高生成的辅导内容的准确性
先查资料,再回答问题
三、知识库建设
3.1 知识片段格式
每条知识包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 唯一标识 |
| name | string | 知识点名称 |
| category | string | 所属分类 |
| content | string | 核心内容描述 |
| time_complexity | string | 时间复杂度 |
| space_complexity | string | 空间复杂度 |
| keywords | []string | 关键词(用于检索) |
| common_errors | []string | 常见错误 |
3.2 JSON 存储示例
{
"id": 2,
"name": "二分查找",
"category": "二分搜索",
"content": "适用条件:有序数组。每次取数组中间元素与目标值比较。如果相等则返回;如果目标值小于中间元素,在左半部分继续查找;否则在右半部分查找。每次将搜索范围缩小一半。",
"time_complexity": "O(log n)",
"space_complexity": "O(1)",
"keywords": ["二分查找", "有序数组", "log n", "分治", "搜索"],
"common_errors": ["边界条件错误(left <= right vs left < right)", "死循环", "未处理空数组"]
}
还是比较简陋的,但是这篇博客的工作主要是跑通rag,先用简单的知识库尝试一下,后面会把每条内容补得详细一点
3.3 覆盖的算法主题
按照计划应该覆盖这些算法,然后包含100多条知识。这篇先在json文件里面填30条,后续会逐步补充到 100 条
| 序号 | 主题 | 已完成 | 计划 |
|---|---|---|---|
| 1 | 哈希表 | 1 | 5 |
| 2 | 二分搜索 | 1 | 5 |
| 3 | 排序算法 | 5 | 10 |
| 4 | 动态规划 | 3 | 10 |
| 5 | 搜索算法(DFS/BFS) | 2 | 10 |
| 6 | 图论基础 | 4 | 10 |
| 7 | 并查集 | 1 | 5 |
| 8 | 二叉树 | 5 | 15 |
| 9 | 前缀和/差分 | 2 | 5 |
| 10 | 双指针/滑动窗口 | 3 | 10 |
| 11 | 贪心算法 | 3 | 10 |
| 12 | 最短路算法 | 0 | 5 |
| 总计 | 30 | 100 |
3.4 存储方式
用 JSON 文件存储。问了ai,500条知识以内json的性能很够,而计划书计划100+条,就这样了,后面对接如果有别的要求再进行更改
四、RAG部分
4.1 关键词检索
生成了一个简单的关键词检索函数,这个问题还是挺大的,先拿着凑合用,后面会进行优化
就是按关键词加score,然后按匹配度排序取k个注入prompt,先跑流程,后面可以升级到向量检索
func SearchKnowledge(db *KnowledgeDB, query string, topK int) []KnowledgeItem {
query = strings.ToLower(query)
for _, item := range db.Topics {
score := 0
if strings.Contains(strings.ToLower(item.Name), query) {
score += 10
}
if strings.Contains(strings.ToLower(item.Category), query) {
score += 5
}
for _, kw := range item.Keywords {
if strings.Contains(query, strings.ToLower(kw)) {
score += 3
}
}
if strings.Contains(strings.ToLower(item.Content), query) {
score += 1
}
}
}
4.2 检索流程
用户提问
↓
提取关键词
↓
遍历知识库,计算每条知识的匹配分数
↓
按分数排序,返回 Top 3 相关知识
↓
注入 Prompt,发给 AI
五、Prompt 增强
把排序拿到的k条知识注入到 Prompt 中:
func BuildPromptWithRAG(problemTitle, problemDesc, userCode, resultType, errorMsg string, knowledge []KnowledgeItem) string {
// 构建知识上下文
knowledgeContext := ""
if len(knowledge) > 0 {
knowledgeContext = "【参考资料】\n"
for i, item := range knowledge {
knowledgeContext += fmt.Sprintf(`
[%d] %s
- 分类:%s
- 核心内容:%s
- 时间复杂度:%s
- 空间复杂度:%s`,
i+1, item.Name, item.Category, item.Content, item.TimeComplexity, item.SpaceComplexity)
if len(item.CommonErrors) > 0 {
knowledgeContext += fmt.Sprintf("\n- 常见错误:%s", strings.Join(item.CommonErrors, "、"))
}
knowledgeContext += "\n"
}
} else {
knowledgeContext = "【参考资料】\n暂无直接相关的参考资料,请根据你的知识回答。\n"
}
return fmt.Sprintf(`你是一个算法学习辅导老师。请参考以下参考资料回答用户问题。
%s
【题目信息】
- 标题:%s
- 描述:%s
【用户代码】
%s
【判题结果】
- 类型:%s
- 信息:%s
请按以下格式输出:
1. 问题定位:用户的代码哪里有问题?
2. 原因分析:为什么会出现这个结果?请尽可能引用参考资料中的内容。
3. 改进方向:如何修改?参考资料中有相关建议的话请优先使用。
注意:
- 不要直接给出完整代码,只提供思路引导
- 如果参考资料中有相关内容,请明确引用`,
knowledgeContext, problemTitle, problemDesc, userCode, resultType, errorMsg)
}
然后其他的部分就是前面两篇的内容,就不赘述了。
简而言之,这篇博客的工作就是整理知识库,让大模型调用之前可以从知识库中参考相关知识,整理好知识上下文,塞到prompt一起丢给大模型,正常的调用和返回结果。
六、效果对比
6.1 测试场景
-
题目:两数之和
-
判题结果:WA
-
用户代码:双层循环 O(n²)
//测试 problemTitle := "两数之和" problemDesc := "给定一个整数数组 nums 和一个目标值 target,请找出和为目标值的那两个整数,并返回下标。" userCode := `vector<int> twoSum(vector<int>& nums, int target) { for (int i = 0; i < nums.size(); i++) { for (int j = 0; j < nums.size(); j++) { if (nums[i] + nums[j] == target) { return {i, j}; } } } return {}; }` judgeResult := "Wrong Answer" errorMsg := "预期输出 {0,1},实际输出 {0,0}"又用回第一篇博客的测试样例了,因为现在知识库内容比较少,用其他自己找的不容易检索到score>0的知识,这样rag部分是无效的,没办法比较加rag前后的输出内容,这个两数之和知识库里有这题,直接用至少可以检索到那条知识(后面优化知识库的内容还有加知识库条数会解决这个问题的QAQ)
反正先跑通这部分再说
6.2 对比
之前的输出情况
PS D:\learn\go\mygo> go run main.go
正在急头白脸地分析中---
1.问题定位:内层循环的初始条件设置不当,未限制两个下标必须互不相同。代码允许i与j取值重合,导致单个元素被重复参与求和运算。
2.原因分析:当i遍历至首元素时,j同样从0开始匹配。若该元素数值的两倍恰好等于target,判断语句立刻成立,程序提前返回{0,0}。这违背了题目选取两个不同位置元素的核心要求。
3.改进方向:将内层循环的起始边界调整为i+1,确保每次比较的都是数组中靠后的元素。该改动可直接避开索引重叠的陷阱,同时消除约一半的无效配对,使算法逻辑与性能均符合标准解法。
经过这篇的种种工作增强之后
1. **问题定位**:用户的内层循环 `for j in range(len(nums))` 允许了 `j` 与 `i` 取相同的值(即 `i == j`),导致算法会优先检查并返回同一个元素相加的情况,从而在测试时直接返回了索引相同的 `[0, 0]`,而非题目要求的两个不同下标。
2. **原因分析**:根据参考资料[1]中提到的“常见错误”,暴力**使用双循环**不仅容易造成性能瓶颈,还极易因为遍历边界设置不严谨引发逻辑错误。本题隐含前提是“不能使用同一个元素两次”,因此两个索引必须不相等(`i != j`)。在你的代码中,当 `i=0` 时,内层 `j` 也从 `0` 开始,此时 `nums[0] + nums[0]` 被率先命中判断条件并直接 `return`,程序就此结束,根本不会继续向后寻找正确的 `{0, 1}`。此外,资料也明确指出双循环时间复杂度较高(O(n²)),在实际判题或数据量稍大时会导致“超时”。
3. **改进方向**:
- **逻辑修正**:调整内层循环的起始位置,例如让 `j` 从 `i + 1` 开始遍历。这样可以天然保证 `i < j`,彻底避免重复使用同一个元素,也能提前修复当前 `{0,0}` 的错误。
- **算法升级**:强烈建议放弃双循环,转而采用参考资料[1]推荐的**哈希表**解法。具体思路为:只遍历一次数组,在每次遍历时,先计算 `target - nums[i]`,并检查该差值是否已存在于哈希表中。若存在,说明找到了匹配项,直接返回哈希表中记录的下标与当前下标;若不存在,再将当前元素的值和索引存入哈希表。该方法时间复杂度仅为 O(n),能完美兼顾正确性与执行效率。
在回答中我们也可以知道确实参考了某条知识
实际上可能确实变专业了,因为字变多了,姑且算有用
七、遇到的问题与解决方案
问题1:知识库内容不够
现象:某些(其实是很多)题目搜不到相关知识
解决:慢慢补,结题前包达到 100 条
问题2:检索匹配不准
现象:现象其实和问题1一样,搜不到,但这里想说的主要问题是单关键词匹配过于狗屎
原因:关键词匹配太粗糙
解决:下一篇博客加向量检索,应该能处理好,下一篇再说
总结:都后面再说
前两篇的那个一直runrunrunrun不出来的问题鉴定为编译卡住了,我抛弃vscode改用goland舒服多了,再也不用终端运行。
八、小结
本篇完成了算法知识库的基础建设。虽然目前只有 30 条,但后续会持续补充。
下一篇将进入混合检索(关键词+向量),让知识库能够根据用户问题智能地找到相关内容。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)