本文是《从零到上线:Agent 应用开发完整路线图》系列第三篇。前两篇讲了整体路线和工具调用设计,这一篇聚焦一个被过度使用的技术——RAG(检索增强生成),搞清楚它到底适合解决什么问题,以及更多时候,一个被忽视的更简单方案就够了。
很多团队的第一反应是:Agent 需要”记忆”或者”知识库” → 上向量数据库 → 做 RAG。这个路径有时候是对的,但相当多情况下,这是在用一个更复杂、更难调试的方案,解决一个本可以用”把数据当工具查询”就能搞定的问题。
这篇文章把两种方案的适用边界讲清楚,并给出两种都能跑的代码示例。
一、先分清两种完全不同的”记忆”
讨论 RAG 之前,要先把”记忆”拆成两个不同的问题,它们的解法完全不同:
短期记忆:当前这一次对话的上下文。模型在多轮对话里记得你前面说过什么,靠的是把历史消息原样塞进 messages 数组——这不需要任何额外组件,上下文窗口本身就是短期记忆。
长期记忆/知识检索:跨会话保留的信息,或者一个庞大到塞不进上下文窗口的知识库(产品文档、历史工单、过往对话记录)。这才是 RAG 真正要解决的问题。
很多人把这两者混为一谈,结果在只需要管理好上下文窗口的场景里,过早引入了向量库。
二、RAG 到底是什么
RAG 的核心循环很简单:
1. 把知识库文档切成小块(chunk),提前转成向量(embedding),存进向量库
2. 用户提问时,把问题也转成向量
3. 在向量库里找出语义最相似的几个文档块
4. 把这几个文档块作为上下文,塞给 LLM 一起生成回答
它解决的问题是:知识库太大,没法每次都整个塞进 prompt,需要先”检索”出最相关的一小部分。
下面是一个可以直接跑的最小 RAG 示例,用 chromadb 做向量库(轻量、本地跑不需要额外服务):
import chromadb
import anthropic
client = anthropic.Anthropic()
chroma_client = chromadb.Client() # 内存模式,实际项目用 PersistentClient 落盘
# 1. 建立知识库集合
collection = chroma_client.create_collection(name="product_docs")
# 2. 灌入文档(实际项目中这一步是离线批处理,文档要先切块)
docs = [
"退货政策:商品签收后7天内,保持完好可申请无理由退货,运费由买家承担。",
"会员等级:消费满1000元升级银卡,满5000元升级金卡,金卡享受免运费特权。",
"发货时效:工作日下单当天发货,周末及节假日顺延至下一工作日处理。",
]
collection.add(
documents=docs,
ids=[f"doc_{i}" for i in range(len(docs))]
)
def search_knowledge_base(query: str, n_results: int = 2) -> str:
"""检索最相关的文档块"""
results = collection.query(query_texts=[query], n_results=n_results)
retrieved = results["documents"][0]
return "\n".join(retrieved)
def answer_with_rag(user_question: str) -> str:
context = search_knowledge_base(user_question)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{
"role": "user",
"content": (
f"根据以下知识库内容回答用户问题,如果知识库没有相关信息,"
f"如实告知不知道,不要编造。\n\n"
f"知识库内容:\n{context}\n\n"
f"用户问题:{user_question}"
)
}]
)
return response.content[0].text
if __name__ == "__main__":
print(answer_with_rag("金卡会员有什么权益"))
这套流程能跑通,但要在生产环境用好,有几个容易被忽视的细节:怎么切块(chunk size)、检索召回不准怎么办、文档更新了向量库要不要重建——这些是 RAG 真正的难点,后面专门讲。
三、什么时候不该用 RAG:直接把数据当工具查询
回到第一篇文章手写的最小 Agent 循环。如果你的”知识库”其实是结构化数据——订单记录、用户信息、商品库存——直接写一个查询工具,通常比做 RAG 更简单、更准确。
判断标准很直接:这份数据有没有明确的查询键(key)?
- 有(订单号、用户ID、商品SKU)→ 直接查询,不需要 RAG
- 没有,是大段非结构化文本,需要”语义相关”才能找到 → 才考虑 RAG
# 不需要 RAG 的场景:有明确查询键
tools = [{
"name": "get_order_detail",
"description": "根据订单号查询订单详情(商品、金额、状态、收货地址)",
"input_schema": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"]
}
}]
def get_order_detail(order_id: str) -> str:
order = db.query(order_id) # 直接精确查询,100% 准确
if not order:
return f"未找到订单 {order_id}"
return f"订单{order_id}:{order.items},金额{order.amount},状态{order.status}"
这种场景下用 RAG 反而是退步:向量检索是”语义相似度”匹配,本质上是模糊检索,而订单号这种结构化查询需要的是精确匹配。把订单数据塞进向量库做语义检索,召回率不仅不会比 WHERE order_id = ? 更好,还会引入”检索到相似但不对的订单”这种本不该存在的错误模式。
一条经验法则:
| 数据特征 | 推荐方案 |
|---|---|
| 有明确查询键(ID、订单号、日期范围) | 直接查询工具(SQL/API) |
| 数据量小,能完整塞进 system prompt | 直接放 prompt,不需要检索 |
| 大段非结构化文本,需要”语义相关”匹配 | RAG |
| 知识库会频繁更新 | 优先考虑直接查询(若结构化)或确保有重建索引的流程 |
| 用户问题措辞多样、查询意图模糊 | RAG 配合较宽松的召回数量 |
实践中很多”看起来需要 RAG”的场景,拆开看其实是结构化数据,换成查询工具能省掉整套向量库的运维成本和召回不准的调试成本。
四、RAG 真正的难点:不是搭起来,是调准
如果你的场景确实是非结构化文本(产品手册、政策文档、历史聊天记录),RAG 是对的方向,但”搭起来能跑”和”检索准确”是两回事。常见的几个坑:
1. Chunk 切分粒度
切得太大,一个 chunk 里混了多个话题,检索命中了但里面 80% 是无关内容,白白占用上下文;切得太小,一句话被拦腰截断,丢失语境。
# 简单但有效的切分策略:按语义边界(段落)切,而不是固定字符数
def chunk_text(text: str, max_chunk_size: int = 500) -> list[str]:
paragraphs = text.split("\n\n")
chunks = []
current = ""
for para in paragraphs:
if len(current) + len(para) > max_chunk_size and current:
chunks.append(current.strip())
current = para
else:
current += "\n\n" + para
if current:
chunks.append(current.strip())
return chunks
按固定字符数硬切是最容易踩的坑,优先按段落、标题这类语义边界切。
2. 检索召回不准怎么办
如果发现模型经常说”知识库里没有这个信息”但实际上文档里明明有,大概率是检索阶段就没召回正确的文档块,而不是模型生成的问题。排查顺序:
- 单独测试检索结果,不要直接看最终回答——把
search_knowledge_base单独跑一遍,人工检查召回的内容是不是真的相关 - 检查 chunk 是否切得太碎,导致关键信息被拆散到了不同 chunk 里
- 增加召回数量(
n_results调大),让更多候选文档进入上下文,代价是 token 成本上升 - 必要时混合关键词检索(BM25)和向量检索,纯语义检索对专有名词、产品型号这类词经常不够准
3. 文档更新了,索引要同步
向量库不会自动感知源文档变了。生产环境必须有一套流程:文档更新 → 重新生成对应 chunk 的向量 → 替换旧向量。这件事容易被忽略,导致 Agent 长期基于过时信息回答。
五、一个完整的判断流程
把上面讲的整理成一张决策图,遇到”要不要上 RAG”的场景直接对照:
这份信息的体量能塞进一次 system prompt 吗?
└─ 能 → 直接放 prompt,不需要检索
└─ 不能 ↓
这份数据有明确的查询键(ID/订单号/日期)吗?
└─ 有 → 写一个直接查询工具(数据库/API),不需要 RAG
└─ 没有,需要"语义相关"才能定位 ↓
确实是大段非结构化文本,且会被频繁问到 → 用 RAG
注意:chunk 切分按语义边界、单独测试召回质量、建立索引更新流程
大部分”我们需要做 RAG”的需求,在这张图的前两步就已经有更简单的答案了。RAG 该用的时候确实好用,但它不是”知识库类需求”的默认选项,而是排除了更简单方案之后的选择。
下一篇进入多 Agent 编排实战,讲 Supervisor 模式什么时候真正需要、并行子任务怎么设计,以及一个常见误区:多 Agent 不是”更强”,而是用额外的复杂度换取任务拆解的清晰度,用错场景反而会更难调试。
发表回复