Day 2 的 Agent 只能读你知道文件名的文件。Day 3 的向量检索能语义定位但没接 LLM。Day 4 让 Agent 自己拥有检索能力 —— 它会自主决定:
你: 我笔记里 Go 的错误处理是怎么讲的?
Agent 内心戏:
第 1 轮:这问题我得先查笔记
🔧 search_notes("Go 错误处理") → 找到 3 段相关内容
第 2 轮:片段有点碎,我读一下完整文件
🔧 read_full_note("go-tips.md") → 拿到全文
第 3 轮:整合信息,引用来源,给出回答
🤖 你的笔记里说:Go 用返回值传错误,函数最后一个返回值通常是 error 类型。
errors.Is / errors.As 用来判断具体错误类型...
(引用来源:go-tips.md)
今天项目变成"多文件"结构。不复杂,但适合 Day 5/6 继续扩展。
day4/
├── ark_embedding.py # embedding HTTP 封装(从 Day 3 拷贝)
├── knowledge_base.py # 🆕 封装 ChromaDB 的访问(search + read + list)
├── tools.py # 🆕 工具 schema + 实现(给 Agent 用)
├── agent.py # 🆕 Agent 主循环(多轮 + Tool Use)
├── ingest.py # 🆕 建库脚本(复用 Day 3 逻辑)
├── notes/ # 你的笔记(.md 文件)
├── chroma_db/ # 向量库(自动生成,gitignore)
└── README.md
建库时:
notes/*.md → 切分 → embedding → chroma_db/
▲
└─ ingest.py
查询时:
用户提问 ──→ agent.py ──→ LLM
▲ │
│ 工具结果 │ tool_calls
│ ▼
tools.py ← 调用 knowledge_base.py ← 读 chroma_db/
给 Agent 提供三个工具,形成"由粗到细"的信息获取梯度:
| 工具 | 作用 | 参数 | 返回 |
|---|---|---|---|
search_notes | 语义检索,找相关片段 | query, top_k | top_k 个 chunk + 来源 |
read_full_note | 读整篇文档 | filename | 完整内容 |
list_notes | 列出所有笔记 | 无 | 文件名列表 |
description 决定何时用哪个。写不清楚,Agent 就会瞎调工具或者不调。今天代码里的 description 是精心打磨过的,你可以直接用;改的时候要注意保持"使用场景"的清晰表达。
cd ~/Documents/practice/agents
mkdir -p day4
cd day4
# 建虚拟环境
python3 -m venv venv
source venv/bin/activate
# 装依赖
pip install openai python-dotenv chromadb httpx
# 复用 Day 3 的 .env
cp ../day3/.env .env
# 用 Day 3 那 3 篇笔记做起点(或者放你自己的进 notes/)
mkdir -p notes
cp ../day3/notes/*.md notes/
然后就跑建库脚本:
python ingest.py
预期看到 3 个文件、55 个 chunk 之类的输出。这一步跟 Day 3 的 demo2_ingest.py 几乎一样,只是把逻辑拆到了 knowledge_base.py 和 ingest.py 里,更工程化。
python agent.py
# 1. 简单查询:只需要 search_notes 一次
你: 我笔记里 Go 的错误处理是怎么讲的?
# 2. 需要综合:Agent 可能先 search 再 read_full_note
你: 帮我总结一下 rag-notes.md 这篇笔记
# 3. 关键词不匹配:考验语义检索的准确性
你: goroutine 之间怎么通信?
# 4. 综合问题:可能需要多次 search
你: 我笔记里都讲了 Agent 的哪些方面?
# 5. 不用工具:LLM 自己就能回答
你: 你好,简单介绍下你自己
# 6. 无关问题:Agent 应该说笔记里没写
你: 我笔记里有讲区块链吗?
今天的 agent.py 特意把每一轮 LLM 调用、每次工具执行都打印出来。你会看到类似:
你: 我笔记里 Go 的错误处理是怎么讲的?
─ 第 1 轮 ─
🤔 LLM 决定调用工具
🔧 search_notes(query='Go 错误处理', top_k=3)
↳ 返回 3 个 chunk:
[1] go-tips.md#2 相似度 0.678
[2] go-tips.md#1 相似度 0.672
[3] go-tips.md#4 相似度 0.629
─ 第 2 轮 ─
🤔 LLM 综合信息,给出回答
🤖 根据你的笔记(go-tips.md):
Go 没有 try/catch 机制,采用**返回值传错误**的风格。函数的最后一个返回值
通常是 error 类型。使用 `%w` 可以包裹底层错误,调用者可以用 errors.Is
或 errors.As 判断具体错误类型...
📊 本次任务:2 轮 LLM 调用 | 消耗 tokens: 1247
注意看几件事:
今天 agent.py 里的 system prompt 特别设计过,几个要点:
你是一个基于用户私人笔记回答问题的知识助手。
原则:
1. 优先使用工具搜索用户的笔记,不要凭记忆回答
2. 如果笔记里没有相关内容,明确告诉用户"我在你的笔记里没找到相关内容"
3. 回答时引用来源(文件名 + chunk 位置)
4. 一次可以并行调用多个工具以提高效率
5. 如果 search_notes 返回的片段不够完整,可以用 read_full_note 读全文
这段 prompt 起到"边界约束 + 行为指导"的作用。改这段能显著改变 Agent 的行为风格 —— 建议你 Day 4 完成后自己改改试试。
让 Agent 在回答里带上 [来源: xxx.md]。好处:
我们的 search_notes 除了返回文本,还返回相似度分数。LLM 能看到 相似度: 0.42,就会意识到"这条不太相关,可能问题匹配不上",从而决定重新 search 或告诉用户没找到。
| 坑 | 解决 |
|---|---|
| Agent 不用工具,凭记忆瞎答 | System prompt 强调"优先使用工具";把工具 description 写得更"必须" |
| 返回过多 chunk,塞爆 context | 限制 top_k=3~5;给 chunk 做长度截断 |
| Agent 死循环重复 search | 加最大轮次(今天代码里限 8 轮);system prompt 里说"最多重试 2 次换关键词" |
| read_full_note 读到超长文件 | 做长度上限(今天代码限 8000 字符),并告诉 LLM 已截断 |
| 幻觉 —— 编造文件名 | 提供 list_notes 让 Agent 先看有哪些文件;工具报错时明确告知 |
knowledge_base.py、tools.py、agent.py 三层cd ~/Documents/practice/agents
git status
git add day4/
git commit -m "Day 4: 组装知识助手 v1(RAG + Tool Use 融合)"
git push
今天的 Agent 有个隐藏 bug:如果你连续聊几十轮,messages 数组会爆炸(还记得 Day 1 token 累计吗?)。
Day 5 要做记忆管理: