Skip to content

RAG 与 Agent

RAG 让模型学会「查资料再回答」,Agent 让模型学会「规划并调用工具完成任务」——两者共享同一根主干:模型的概率生成 + 外部系统提供的确定性事实。对应论文原理见 RAGReAct 精读。本文给出从零搭建到可评测、可上线的完整方法。

一、先想清楚:什么时候用 RAG / 长上下文 / 微调

需求首选方案为什么
知识常变、量大、要可溯源RAG改库不改模型,回答带引用
单次会话内读整本长文档长上下文窗口语境连续,但要防「中间丢失」与成本
语气/格式/领域术语要「定型」微调(见 模型微调)把行为写进权重,不靠每次提示词
精确计算/状态操作工具/代码(Agent)模型不擅长的交给确定系统

RAG 与长上下文不是对立:答案权威源在外部且常变 → RAG;权威内容就是给模型一次性读完的手册 → 长上下文。组合拳(先检索再塞窗口)最常见。

二、RAG 管线全景:五环一图流

文档 → ①加载清洗 → ②切分(chunk)→ ③嵌入 + 向量库 → ④检索(召回+重排)

回答 ← ⑥生成(带引用) ← ⑤组装上下文(检索片段 + 指令)
  • 加载清洗:PDF/HTML/表格各用合适解析器(表格别转纯文本丢结构),去页眉页脚、去模板噪声;
  • 切分:目标不是「每块一样大」,而是块内语义完整、块间可定位——按结构(标题/段落/表格/代码块)切比按固定字符切好;相邻块加 10-15% 重叠,防句子在接缝被切开;中文注意按句/按语义单位;
  • 嵌入:把文本映射成向量,相似语义 = 向量距离近;选嵌入模型看检索任务效果而非排行榜幻觉;
  • 检索:召回(粗筛候选) + 重排(精排 top-k),见下节;
  • 组装:把 top-k 片段 + 「忠实于片段」的指令拼进上下文,标注片段来源序号;
  • 生成:要求逐句引用出处编号、禁止编造;生成后做「引用可查证」检查(见评测)。

三、检索:召回与重排

  • 召回三件套,建议混合:
方法原理强项短板
BM25(稀疏)词频 + 逆文档频率 + 文档长度归一术语精确、无训练同义改写就失配
稠密检索(向量)嵌入余弦相似度语义泛化冷门术语/长尾差
混合 + RRF各取前 k 名按 1/(60+rank) 合并计分互为补充需调权重
  • 重排(rerank):召回给 50-100 条,用交叉编码器/排序模型精排到 3-10 条——重排是「便宜大碗」的精度提升点;
  • 召回前的 query 变换(性价比极高):
    • 查询改写(query rewrite):把模糊问句改写为检索友好的关键词问句;
    • 多查询(multi-query):同一问题拆 2-3 种问法并行检索,合并去重;
    • HyDE:先让 LLM 生成假设答案,再用答案去检索(答案比问题更像文档语言);
  • 迭代检查顺序:先看召回再怪生成——生成差往往因 top-k 没命中正确片段。

四、向量化与向量数据库选型

  • 数据量从百到亿级,选型梯度:
场景方案说明
原型/教学FAISS / Chroma内存/单机足够,零运维
已有 PostgreSQLpgvector与业务数据同库,支持混合检索,少一个中间件
万级以上 + 高并发Milvus / Qdrant / Weaviate分布式、过滤、别名/多租户、监控齐全
  • 索引默认 HNSW(图式 ANN 索引):召回率/速度/内存三者的默认平衡点;数据量级变化后重建索引并验收召回率(暴力检索对比抽样);
  • 生产必做:向量库与源文档的版本对齐(文档更新 → chunk 重新切分入库 → 旧向量删除),并保留「片段 → 源文件行号」的溯源元数据用于引用回查。

五、RAG 评测:没有数字就没有迭代

测什么常用指标/方法
检索层该片段是否被召回Hit Rate、Recall@k、MRR
生成层答案是否忠实于片段、是否可溯源忠实度(faithfulness)、引用命中
端到端最终答案对不对答案正确率、LLM-as-judge 或人工标注
成熟套件综合RAGAS(faithfulness / answer relevancy / context precision 等)
  • 工程建议:先建 50-100 条金标问答对(每条带「标准证据片段」),跑检索 Recall@k 定位「切分/嵌入」问题,再跑端到端定位「组装/提示」问题;
  • 上线后定期采样重放,文档更新后全量回归(详见 AI 工程化 评测门禁)。

六、从 RAG 到 Agent:工具、规划与循环

  • Agent = LLM + 工具 + 循环:模型决定下一步调哪个工具,代码执行并回填结果,循环直到任务完成;
  • 最小循环即 ReAct:思考 → 行动(调工具)→ 观察(工具返回)→ 再思考;
  • 实现三要素:
    1. 工具封装:每个工具 = 名称 + 描述 + JSON schema 参数(描述要写给「模型」看:何时用、参数怎么填);
    2. 循环控制:最大步数、异常重试、目标达成判断(让模型显式输出 FINISH);
    3. 记忆:会话内短期记忆(消息历史)+ 长期记忆(向量库存事实/偏好);
  • 框架速查:
框架定位适合
LangChain组件齐全的生态胶水快速拼装、调研
LlamaIndex数据/RAG 为中心文档密集型应用
LangGraph图式状态机,循环可控生产级复杂流程
CrewAI / AutoGen多智能体协作角色分工试点
MCP(协议)工具标准接口(2025+ 主线)工具接入的开放标准
  • 忠告:框架是脚手架不是银弹——先用手写 50 行循环跑通,再加框架;Agent 最难的从来不是调用工具,而是步数爆炸、错误传播与不可控,必须配步数上限 + 工具权限最小化 + 审计日志(安全见 Prompt 工程 注入防线与 AI 工程化 可观测)。

6.1 ReAct 最小循环实测(deepseek-chat, 2026-09-04)

手写一个 ReAct 循环并真实跑通。为呼应本页主题,工具是一个「本地笔记检索」search_notes(模拟 RAG 检索层):模型上下文里不放笔记原文,事实只能靠检索工具拿到,禁止编造。实测轨迹与三个真实踩坑见代码与注释:

python
# ReAct 最小循环 —— 真实可跑版(DeepSeek deepseek-chat)
# 依赖: python3 + requests; 环境变量 DEEPSEEK_API_KEY
# 用法: export DEEPSEEK_API_KEY=sk-... && python3 本文件
import os, json, re, requests

API = "https://api.deepseek.com/chat/completions"
MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat")

# 模拟「内部技术笔记库」: 只有检索工具能读到, 模型上下文里不放原文
NOTES = [
    {"title": "python", "body": "Python 是解释型高级语言, 1989 年由 Guido van Rossum 开始设计, 1991 年发布, 以易读与生态著称。"},
    {"title": "golang", "body": "Go(常称 golang) 是 Google 于 2009 年发布的静态编译语言, 以并发原语与编译速度著称。"},
    {"title": "reactjs", "body": "React 是 Facebook 于 2013 年发布的前端 UI 库, 组件化 + 虚拟 DOM。"},
    {"title": "llama", "body": "Llama 是 Meta 开源的大模型系列, 首个版本发布于 2023 年。"},
]

TOOL_SPEC = [{"type": "function", "function": {
    "name": "search_notes", "description": "在内部技术笔记库按关键词检索, 返回命中笔记的标题与要点。要查编程语言/框架的事实背景时才用, 不要凭空编造。",
    "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "检索关键词, 如 python / golang"}}, "required": ["query"]}}}]

def execute_tool(args):
    q = args["query"].lower()
    hits = [n for n in NOTES if q in n["title"].lower() or q in n["body"].lower()]
    if not hits:
        return {"ok": False, "error": f"没有命中 {q!r} 的笔记"}
    return {"ok": True, "hits": [f"《{n['title']}》: {n['body'][:120]}" for n in hits]}  # 结果加工, 控制回灌

def chat(messages, tools=None):
    payload = {"model": MODEL, "messages": messages, "stream": False}
    if tools:
        payload["tools"] = tools   # 踩坑: 不挂 tools, 模型根本看不见工具, 只会文本模拟调用
    r = requests.post(API, headers={"Authorization": "Bearer " + os.environ["DEEPSEEK_API_KEY"]},
                      json=payload, timeout=60)
    r.raise_for_status()
    return r.json()["choices"][0]["message"], r.json()["usage"]["total_tokens"]

def run_react(goal, max_steps=12):
    state = {"trace": [], "done": False, "reason": "", "tokens": 0}
    # 踩坑: 冗长 system「教模型 ReAct」会诱导它在正文里写 action/action_input 文本模拟,
    # 反而压制原生 tool_calls; 规则并入首条消息 + 让工具 schema 自己说话即可。
    prompt = goal + ("\n要求: 必须调用 search_notes 工具分别查到 python 与 golang 的笔记后再作答;"
                     " 年份与类型必须与检索结果一致; 最终只输出 JSON: "
                     '{"python":"一句话", "golang":"一句话"}')
    history = [{"role": "user", "content": prompt}]
    state["trace"].append("THINK: " + goal)
    step = 0
    while not state["done"] and step < max_steps:
        step += 1
        msg, toks = chat(history, tools=TOOL_SPEC)   # ACT: 模型决策调哪个工具
        state["tokens"] += toks
        history.append(msg)
        if msg.get("tool_calls"):
            for tc in msg["tool_calls"]:             # OBSERVE: 执行并回填
                args = json.loads(tc["function"]["arguments"])
                result = execute_tool(args)
                state["trace"].append(f"ACT search_notes({args['query']}) -> " +
                                      ("; ".join(result["hits"]) if result["ok"] else result["error"]))
                history.append({"role": "tool", "tool_call_id": tc["id"],
                                "content": json.dumps(result, ensure_ascii=False)})
        else:
            # 无工具调用 => 视为最终答复, 程序化验证(年份必须来自检索)
            j = json.loads(re.search(r"\{.*\}", msg["content"], re.S).group(0))
            ok = ("1989" in str(j.get("python", "")) and "2009" in str(j.get("golang", "")))
            state["trace"].append(f"ANSWER: {msg['content']}")
            if ok:
                state["done"], state["reason"] = True, "validated(年份均有检索支撑)"
            else:
                # 踩坑: 打回反馈绝不能写正确答案(=评测数据泄漏), 只描述缺什么, 事实让 Agent 自己查
                missing = [k for k in ("python", "golang") if not re.search(r"\d{4}", str(j.get(k, "")))]
                history.append({"role": "user",
                                "content": f"验证失败: 字段 {missing} 缺少年份事实。请先 search_notes 检索对应笔记再补全。"})
    if not state["done"]:
        state["reason"] = "step_budget_exhausted"
    return state

if __name__ == "__main__":
    assert os.environ.get("DEEPSEEK_API_KEY"), "请先 export DEEPSEEK_API_KEY=sk-..."
    s = run_react("分别查清 python 与 golang: 各自是什么类型、诞生年份各是多少。")
    for t in s["trace"]:
        print(" *", t)
    print("done:", s["done"], "| reason:", s["reason"], "| tokens:", s["tokens"])

# ---- 实测输出(2026-09-04, deepseek-chat, 1144 tokens) ----
#  * THINK: 分别查清 python 与 golang: 各自是什么类型、诞生年份各是多少。
#  * ACT search_notes(python) -> 《python》: Python 是解释型高级语言, 1989 年由 Guido van Rossum 开始设计...
#  * ACT search_notes(golang) -> 《golang》: Go(常称 golang) 是 Google 于 2009 年发布的静态编译语言...
#  * ANSWER: {"python":"Python 是解释型高级语言,1989 年开始设计,1991 年发布...","golang":"Go 是 Google 于 2009 年发布的静态编译语言..."}
#  done: True | reason: validated(年份均有检索支撑) | tokens: 1144
  • 实测中三个真实踩坑(都已写进代码注释):工具没挂载(请求里少了 tools 字段,模型看不见工具,只能用文本假装调用)、system 过度教学(教它 ReAct 反而诱导出 action/action_input 文本格式,盖过原生 function calling)、验证反馈泄题(把正确答案写进"验证失败"提示 = 评测数据泄漏,Agent 不再检索直接抄答案)。这三个坑分别对应本页「工具封装」「循环控制」与下页「评测集/验证器」的工程要点。

七、常见失败模式与调试

症状根因方向先查
答案像没看过资料检索没召回正确片段打印 top-k 片段人工看命中
引用对不上chunk 定位粒度粗 / 溯源丢失chunk 元数据与切分策略
问句一换就不行query 与文档语言脱节上 query rewrite / 多路召回
表格/代码问不好切分把结构切碎结构化块按类型切分+单独检索
Agent 死循环/乱调工具工具描述不清 / 缺步数上限审工具 schema 与终止条件
越问越错上下文污染(检索噪声进提示)过滤低相关片段、调重排

八、踩坑清单

  1. 跳过清洗直接切分:PDF 噪声、页眉页脚进库,检索质量崩在源头;
  2. 固定字符切分:句子/表格/代码被拦腰斩断,召回与忠实度双降;按结构切 + 重叠;
  3. 只上向量不做 BM25 混合:术语型问题(型号/代码标识)召回全灭;
  4. 召回看数量不看质量:top-k 放大只会把噪声塞进上下文,先重排再做质量检查;
  5. 忘 query rewrite:用户口语问法检索不到文档术语,先改写再检索;
  6. 引用是「贴上去的」:不做来源校验,幻觉引用照样出现;逐句核对来源;
  7. 上线不评测:不建金标、不回放,文档一更新效果断崖却无法定位;
  8. Agent 不给步数上限与权限墙:生产事故多发于此;
  9. 迷信框架:框架隐藏细节,debug 困难;先手写循环理解再上框架;
  10. 忽视成本:每轮 Agent 循环 = N 次 LLM 调用,失控时账单感人——加缓存与预算闸门。

继续学习:RAG 与 Agent 都建立在 LLM 能力之上,原理见 LLM 原理;需要改变模型本身行为(领域格式稳定输出)时进入 模型微调;把整套系统跑稳、跑便宜、可观测见 AI 工程化

基于 VitePress 构建 · 内容以知识共享方式沉淀