写在前面:

上一篇博客我完成了多场景 Prompt 模板的设计,针对 WA、CE、TLE 等不同判题结果给出了不同的提问方式。但我们都知道,ai有时后的回答会奇奇怪怪的,比如讲两数之和时,它可能自己编一个不存在的解法。这是因为大模型的知识来自训练数据,不一定包含我们题目库里的特定内容。那如果我们希望解决这个问题,那就需要构建一个知识库,让大模型给我们生成辅导讲解的时候可以进行参考,让ai尽量不要乱讲。所以这一篇,我来做 RAG(检索增强生成),让ai先查资料再回答。

一、本篇目标

对应任务书指标

  • 建设算法知识库,整理不少于 100 条知识片段

  • 知识内容覆盖不少于 10 个常见算法主题

建一个存放算法知识的地方,后续让 AI 先查这里再回答问题。

需要做的三件事

  1. 建一个知识库(存算法知识点和题解)

  2. 用户提问时,先从知识库里找相关内容

  3. 把找到的内容和问题一起发给 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 条,但后续会持续补充。

下一篇将进入混合检索(关键词+向量),让知识库能够根据用户问题智能地找到相关内容。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐