ReAct 基线回顾
ReAct(Reasoning + Acting)将推理和行动交错进行:Thought → Action → Observation 循环,直到任务完成。这是当前 Agent 的基础范式,但它有明显局限:线性推理不回溯、每步决策短视、缺乏自我纠错能力。
| 组件 | 作用 | 局限 |
| Thought | 分析当前状态,决定下一步 | 只看当前上下文,无历史反思 |
| Action | 调用工具执行操作 | 无法撤销错误行动 |
| Observation | 获取工具返回结果 | 失败时缺乏系统性反思 |
Reflexion:语言反思试错学习(Shinn et al. 2023)
Reflexion 的核心洞察:用自然语言反馈(verbal reinforcement)指导后续推理,不需要更新模型权重。这是一种「在上下文中学习」的试错机制。
01
Trial(尝试)
Actor 生成完整轨迹(trajectory):Thought → Action → Observation 序列,尝试完成任务
02
Evaluation(评估)
Evaluator 对轨迹打分:任务是否完成?哪里出错了?生成 reward 信号
03
Self-Reflection(反思)
模型生成语言反思:哪里错了?为什么错?下次怎么改?,例如「上一次我误把加法当乘法,下次应该先确认运算符」
04
Memory(记忆)
反思存入情景记忆(episodic memory),下次尝试时将历史反思作为上下文注入
05
Next Trial(重试)
带着反思重新执行,通常 2-3 次迭代后成功率显著提升
关键机制:反思是自然语言反馈,不是梯度更新;模型通过「阅读自己之前的错误总结」来改进,不需要微调。
| 角色 | 职责 |
| Actor | 执行任务,生成轨迹 |
| Evaluator | 判断成败,给出反馈(可以是规则、测试用例或另一个 LLM) |
| Self-Reflection | 生成具体改进建议,存入记忆 |
Self-Ask:显式自问自答分解(Press et al. 2022)
Self-Ask 让模型在回答主问题前,显式向自己提出 Follow-up 子问题,逐个解答后再回答主问题。这是一种显式的问题分解策略。
示例流程:
- 主问题:「爱迪生发明电灯泡时,谁是当时的美国总统?」
- Follow-up 1:「爱迪生什么时候发明电灯泡?」→ 搜索:1879 年
- Follow-up 2:「1879 年的美国总统是谁?」→ 搜索:Rutherford B. Hayes
- 最终答案:Rutherford B. Hayes
核心优势
- 组合泛化:把复杂组合问题拆解为简单子问题,提升多跳推理能力
- 可与搜索结合:每个子问题可以调用搜索引擎获取答案,不依赖模型记忆
- 可解释:推理路径完全透明,用户能看到每一步分解
与 ReAct 的区别
ReAct 在行动中思考,Self-Ask 在行动前先分解问题;Self-Ask 的子问题是显式的「问题」形式,而 ReAct 的 Thought 是自由文本。
Tree of Thoughts(ToT):树状搜索推理
CoT 是线性推理,一旦走错就无法回头;ToT 把推理建模为树结构搜索,维护多个候选路径,评估后选择最有前景的分支继续探索,不 promising 的分支可以剪枝回溯。
| 组件 | 说明 |
| Thought 生成 | 从当前状态生成多个候选下一步(BFS 宽度或 DFS 深度) |
| 状态评估 | 用 LLM 给每个 thought 打分(「这个思路能解决问题吗?0-10 分」) |
| 搜索算法 | BFS(逐层扩展)或 DFS(深度优先 + 剪枝) |
| 回溯 | 某条路径分数低则回退到上一分支点,探索其他分支 |
面试对比:CoT 是单链路「一条路走到黑」,ToT 是「多路径并行探索 + 评估剪枝」。ToT 适合需要规划、搜索、试错的问题(如 24 点游戏、创意写作、数学证明),但计算成本高(需要多次 LLM 调用生成和评估)。
LATS(Language Agent Tree Search):蒙特卡洛树搜索 + LLM
LATS 将传统游戏 AI 中的 MCTS(Monte Carlo Tree Search)与 LLM 结合,比 ToT 更有原则性(principled)的探索。
| MCTS 阶段 | LATS 实现 |
| Selection(选择) | 从根节点用 UCB 策略选择最有价值的子节点,平衡探索与利用 |
| Expansion(扩展) | LLM 生成多个候选动作(thought/action),扩展叶子节点 |
| Simulation(模拟) | 从新节点用 LLM 做 rollout(快速走子),估计未来回报 |
| Backpropagation(回传) | 将轨迹最终结果(成功/失败)回传更新路径上所有节点的价值估计 |
| Reflection(反思) | 失败轨迹后生成反思,在下一次选择时避免相同错误 |
LATS 的优势:LLM 同时充当价值函数(评估状态)和策略(生成动作),反思机制让失败经验转化为后续搜索的指导。比 ToT 的 BFS/DFS 有更坚实的探索-利用平衡理论。
Plan-and-Execute:先规划再执行
Plan-and-Execute 将 Agent 分为两个独立阶段:Planner 先生成完整计划(步骤列表),Executor 逐步执行;执行中如果失败,触发 Replanner 重新规划。
01
Plan(规划)
LLM 分析任务,生成完整步骤列表:1. 搜索用户信息 2. 查询订单 3. 计算金额 4. 生成报告
02
Execute(执行)
按顺序执行每一步,每步调用对应工具,记录结果
03
Replan(重规划)
某步失败或发现新信息时,重新生成/更新剩余步骤计划
与 ReAct 对比
| 维度 | ReAct | Plan-and-Execute |
|---|
| 决策粒度 | 每步独立决策,短视 | 全局规划,视野更长 |
| 灵活性 | 高,随时调整 | 较低,需要触发重规划 |
| 成本 | 每步都调用 LLM 决策 | 规划调用一次,执行步骤较简单 |
| 适用场景 | 探索性、动态环境、简单任务 | 长周期、步骤明确、复杂 API 工作流 |
范式对比与选型
| 范式 | 记忆 | 搜索 | 自我批判 | 最适合场景 |
| ReAct | 对话历史 | 无(线性) | 无 | 简单 QA、工具调用、快速原型 |
| Reflexion | 情景记忆(反思) | 多次尝试 | 显式语言反思 | 代码生成、数学推理、需要迭代改进 |
| Self-Ask | 子问题答案 | 无(分解) | 无 | 组合式 QA、多跳推理、事实核查 |
| ToT | 树状态 | BFS/DFS | 状态评估打分 | 规划问题、创意生成、需要搜索的推理 |
| LATS | MCTS 树 + 反思 | MCTS(UCB) | 失败轨迹反思 | 复杂决策、游戏、需要探索-利用平衡 |
| Plan-and-Execute | 计划状态 | 有限(重规划) | 失败重规划 | 长周期 API 工作流、ETL、明确步骤任务 |
选型口诀:简单 QA→ReAct;多步数学→ToT/LATS;迭代改进→Reflexion;复杂 API→Plan-Execute;组合 QA→Self-Ask。实际系统中常混合使用(如 Plan-and-Execute 内每步用 ReAct,加上 Reflexion 反思机制)。
范式选择的工程考量
理论范式和生产系统之间有很大距离。选择范式时除了问题结构,还要考虑以下工程约束:
| 约束 | 影响 | 建议 |
| LLM 调用成本 | ToT/LATS 每步多次调用,成本是 ReAct 的 5-20 倍 | 简单任务不要过度工程化,先用 ReAct 基线 |
| 延迟要求 | 多路径搜索和迭代反思显著增加延迟 | 在线低延迟场景用 ReAct/Plan-Execute;离线任务可用 ToT/Reflexion |
| 确定性要求 | 搜索类范式(ToT/LATS)输出路径不确定 | 需要可复现的工作流用 Plan-and-Execute,规划阶段可人工审核 |
| 工具可靠性 | 工具返回错误时,ReAct 容易陷入死循环 | 加最大迭代次数限制 + Reflexion 式错误反思 |
| 可观测性 | 多分支搜索难以 debug | 记录完整 thought/action/observation 轨迹,支持 replay |
混合范式:生产系统的真实架构
实际 Agent 系统几乎不会只用单一范式,而是多层嵌套组合。典型的混合架构:
01
外层:Plan-and-Execute
接收用户任务后,Planner 生成高层步骤计划,确定需要哪些子任务
02
中层:Self-Ask 分解
每个复杂子任务用 Self-Ask 分解为更小的可执行单元
03
内层:ReAct 执行
每个原子步骤用 ReAct 循环:思考→调用工具→观察结果,直到子任务完成
04
失败:Reflexion 反思
子任务失败时,触发 Reflexion 生成反思,调整策略重试(最多 N 次)
05
重规划
如果某步反复失败或发现计划不合理,回到 Planner 更新剩余步骤
LangGraph、AutoGPT、MetaGPT 等框架本质上都是在实现这种混合范式:外层规划 + 内层 ReAct + 失败反思 + 重规划。理解各范式的职责边界比记住单个范式更重要。
记忆如何增强各范式
| 记忆类型 | 内容 | 增强的范式 |
| 短期记忆(上下文) | 当前对话/任务的 thought/action/observation 历史 | 所有范式都依赖 |
| 情景记忆(Episodic) | 过去的任务轨迹、成功/失败经验、Reflexion 反思 | Reflexion(核心依赖)、LATS |
| 语义记忆(Semantic) | 领域知识、工具文档、API 说明、事实知识 | 所有范式(通过 RAG 注入) |
| 程序记忆(Procedural) | 技能、策略、SOP、常见任务的标准流程 | Plan-and-Execute(从程序记忆生成计划) |
Reflexion 的情景记忆是关键创新——它让 Agent 能「从自己的错误中学习」,而不依赖模型微调。每次失败后的语言反思,本质上是在程序记忆中积累「什么情况下不要做什么」的策略知识。
范式使用的失败模式与防护
高级范式带来能力提升的同时,也引入新的失败模式,工程中必须做防护:
| 范式 | 典型失败模式 | 防护措施 |
| ReAct | 无限循环(同样的 Action-Observation 重复)、动作空间爆炸 | 最大迭代次数限制(如 15 步)、检测重复动作强制终止、超时控制 |
| Reflexion | 反思本身错误、过度反思导致上下文膨胀、反思记忆冲突 | 限制反思次数(通常 2-3 次)、记忆只保留最近 N 条最相关反思、反思后要有 evaluator 验证改进 |
| ToT/LATS | 搜索树爆炸、评估器偏差导致错误剪枝、计算成本失控 | 限制搜索宽度(每步最多 3-5 个分支)和深度(最多 5-8 层)、总 token 预算控制、beam search 替代全搜索 |
| Plan-and-Execute | 初始计划质量差导致反复重规划、计划步骤粒度不均、重规划震荡 | Planner 输出后用 Critic 审核计划、限制最大重规划次数、步骤粒度标准化 |
| Self-Ask | 子问题无限递归、子问题答案错误级联 | 限制子问题深度(最多 3 层)、子问题答案验证、避免重复问相同问题 |
所有搜索/迭代类范式都必须有「熔断机制」:步数限制、token 预算、超时时间。不要让 Agent 无限循环烧钱。
Q: Reflexion 的反思存在哪里?
Reflexion 的反思存在情景记忆(episodic memory)里,通常是一个持久化的列表结构(内存或向量库)。每次尝试失败后,模型生成一段自然语言反思文本,描述「这次错在哪、为什么错、下次怎么做」,然后追加到记忆中。
下一次尝试时,系统会把历史反思(最近几次最相关的)作为额外上下文注入 prompt,模型「读到自己之前的错误总结」,从而避免重蹈覆辙。注意这是在上下文中学习(in-context learning),不更新模型权重。
存在 episodic memory,以自然语言文本形式追加,下次尝试时作为上下文注入,属于 in-context 改进而非微调。
Q: ToT 和 CoT 的区别?
| 维度 | CoT(思维链) | ToT(思维树) |
|---|
| 结构 | 线性链,单路径 | 树结构,多路径 |
| 回溯 | 无,错了就错了 | 可以剪枝回溯到上一分支 |
| 评估 | 不评估,直接走到底 | 每个 thought 用 LLM 打分 |
| 搜索 | 无 | BFS/DFS 搜索最优路径 |
| 成本 | 低(1 次 LLM 调用) | 高(生成+评估多轮 LLM 调用) |
| 适用 | 有明确解法的问题 | 需要规划/探索/试错的问题 |
类比:CoT 是「闭着眼睛走直线」,ToT 是「每到岔路口就评估哪条路好,走错了退回来换路」。
CoT 线性单路径无回溯,ToT 树状多路径可回溯剪枝有状态评估;CoT 成本低适合简单推理,ToT 成本高适合复杂规划搜索。
Q: Plan-and-Execute vs ReAct 什么时候用哪个?
用 ReAct 当:
- 任务简单、步骤少(1-5 步)
- 环境动态变化,需要实时响应
- 不确定需要多少步,需要探索
- 快速原型开发
用 Plan-and-Execute 当:
- 任务复杂、步骤多(5 步以上)
- 步骤依赖关系明确(先做 A 再做 B 再做 C)
- 需要全局最优(如旅行规划、多步骤 API 编排)
- 需要可审计、可预测的执行路径
工程实践:实际系统中常见混合模式——Plan-and-Execute 作为外层框架,每个步骤内部用 ReAct 处理子任务;执行失败时触发 Replanner 重新规划剩余步骤,而不是从头开始。
简单动态探索任务用 ReAct;长周期结构化工作流用 Plan-and-Execute;复杂系统可外层 Plan 内层 ReAct + 失败重规划。
基础 RAG Pipeline 回顾
RAG(Retrieval-Augmented Generation)通过「检索相关知识 + 生成回答」让 LLM 回答有事实依据,减少幻觉。基础 pipeline 分为两个阶段:
01
Chunk(分块)
将文档切分为固定大小的文本块(如 512 tokens)
02
Embed(嵌入)
用 embedding 模型将每个 chunk 转为向量
03
Store(存储)
向量存入向量数据库(FAISS/Milvus/Pinecone/PGVector)
04
Query Embed
用户查询也用同一模型转为向量
05
Retrieve(检索)
向量相似度搜索(cosine),取 Top-K 最相关 chunk
06
Generate(生成)
将检索到的 chunk 塞进 prompt,让 LLM 基于上下文回答
分块策略(Chunking Strategies)
分块是 RAG 质量的第一步——分块不好,后面再怎么优化检索都救不回来。
| 策略 | 方法 | 优点 | 缺点 |
| 固定大小分块 | 按 token 数切分(如 512 tokens),加 overlap(50-100 tokens) | 简单、实现快 | 可能切断语义边界(句子/段落中间断开) |
| 语义分块 | 计算相邻句子 embedding 相似度,相似度骤降处作为边界 | 保持语义完整 | 计算成本高,需要调参阈值 |
| 递归分块 | 按分隔符层级递归切:段落 → 句子 → 词/token | 平衡语义和大小 | 规则较多,需要自定义分隔符 |
| Parent-Document | 检索小 chunk(精确匹配),返回时给 LLM 更大的父 chunk | 兼顾精度和上下文完整 | 需要存储两层结构 |
| Sentence-Window | 检索命中中心句,返回时扩展为前后 N 句窗口 | 简单有效,保持局部上下文 | 窗口大小需要调 |
| 结构化分块 | 按 Markdown/HTML 结构切:标题、章节、代码块、列表 | 保留文档结构信息 | 依赖文档格式解析 |
实践建议:先从递归分块 + 适当 overlap 起步;遇到检索精度问题再尝试 Parent-Document 或语义分块;代码/API 文档一定要用结构化分块保留代码块完整性。
检索方法:从纯向量到混合检索 + Rerank
纯向量检索(cosine similarity)对语义相似好,但对精确关键词(ID、错误码、专有名词)表现差。
| 方法 | 原理 | 优势 |
| Dense Retrieval(稠密/向量检索) | Embedding 模型将文本映射为向量,cosine 相似度排序;支持多向量(ColBERT 晚交互:query 和 doc 各分 token 向量,细粒度匹配) | 语义匹配强,同义/近义识别好 |
| Sparse Retrieval(稀疏/关键词检索) | BM25(TF-IDF 改进):词频-逆文档频率,精确 term 匹配 | 精确 ID/错误码/专有名词强,无需训练 |
| Hybrid Retrieval(混合检索) | Dense + Sparse 两路召回,RRF(Reciprocal Rank Fusion)融合:score = Σ 1/(k + rank_i),k 通常取 60 | 互补优势,对单一编码器失败鲁棒 |
| Reranking(重排序) | 先 Bi-encoder 召回 N 个候选(如 N=100),再用 Cross-encoder 对 query+doc 对逐对打分精确排序,取 Top-K(如 K=5)给 LLM | 精度远超 Bi-encoder,是 RAG 效果提升最显著的单点优化 |
RRF 公式直觉
k=60 是经验值。rank 越小(排名越靠前),贡献越大:rank=1 时贡献 1/61≈0.016,rank=60 时贡献 1/120≈0.008。如果一个文档在两路召回都排第一,RRF 分数最高。RRF 的好处是不需要归一化分数,直接用排名融合,简单鲁棒。
Bi-encoder vs Cross-encoder
Bi-encoder(embedding 模型):query 和 doc 分别编码为独立向量,速度快但精度受限,适合大规模召回;Cross-encoder(reranker):query 和 doc 拼接一起输入模型,做交叉注意力,精度极高但每次只能算一对,慢,适合小批量精排。典型 Reranker:bge-reranker、Cohere Rerank。
查询变换(Query Transformations)
用户原始查询往往不完美:歧义、太抽象、太复杂。查询变换在检索前改写查询,提升召回质量。
| 技术 | 做法 | 核心思想 |
| Query Rewriting | LLM 将模糊/复杂查询改写为更精确的检索查询 | 消除歧义,补充关键词 |
| Multi-Query Retrieval | 生成 3-5 个不同角度的改写查询,每个都检索,结果去重融合 | 从多个视角覆盖相关文档 |
| HyDE(Hypothetical Document Embedding) | 先让 LLM 生成一个「假设答案」,用这个假设答案的 embedding 去检索(而非原 query embedding) | 答案的 embedding 比问题 embedding 更接近真实文档(因为文档是答案的分布) |
| Step-back Prompting | 生成一个更抽象的「退一步」问题(如问「某公式参数含义」退到「相关物理原理是什么」),同时检索原查询和 step-back 查询 | 抽象问题能召回高层概念/原理文档,补充具体问题的上下文 |
| Sub-question Decomposition | 把复杂问题分解为子问题(Self-Ask 风格),每个子问题分别检索 | 多跳问题拆成单跳,每跳精确检索 |
HyDE 直觉:用户问「量子隧穿的实际应用」,query embedding 和讲应用的文档可能不够近;但让 LLM 先写一段「量子隧穿应用包括扫描隧道显微镜...」,这段假设答案和真实文档的语义空间更接近,检索效果更好。
Graph RAG(微软 2024)
向量检索擅长局部语义相似,但对「全局主题是什么」「实体间有什么关系」这类全局性问题无能为力。Graph RAG 通过构建知识图谱解决这个问题。
| 阶段 | 做法 |
| 1. 图谱构建 | LLM 从文档中抽取实体(nodes)和关系(edges),构建知识图谱 |
| 2. 社区检测 | 用 Leiden 算法在图谱上检测社区(紧密连接的实体簇) |
| 3. 社区摘要 | 对每个社区生成层次化摘要(低层细节、中层主题、高层概览) |
| 4. Local Query | 局部查询:搜索相关实体 → 构建子图 → 用相关社区摘要生成答案 |
| 5. Global Query | 全局查询:map-reduce 遍历所有社区摘要,汇总回答全局性问题(如「这份报告的主要主题是什么」) |
Graph RAG 的价值:向量 RAG 回答「X 是什么」很好,但回答「整体在讲什么」「A 和 B 有什么关系」很差——因为这些关系跨越多个 chunk,向量相似度无法捕捉。Graph RAG 用实体-关系图显式建模这些连接。代价是构建成本高(需要多次 LLM 调用抽实体和生成摘要)。
多跳检索与 IR-CoT
复杂问题需要多轮检索:先检索一部分,根据结果决定下一步查什么,交替检索和推理。
01
Retrieve(检索)
根据当前问题/推理状态检索相关文档
02
Reason(推理)
阅读检索到的内容,推理已获得什么信息、还缺什么
03
Generate Next Query
如果信息不足,生成下一个检索查询
04
Loop
重复 1-3 直到获取足够信息回答问题
这本质上就是 ReAct 范式在 RAG 中的应用:Interleaving Retrieval with Chain-of-Thought Reasoning(IR-CoT)。
RAG 评估指标体系
RAG 评估分两个维度:检索质量和生成质量。不要只看最终回答对不对——要分别诊断问题出在检索还是生成。
| 维度 | 指标 | 含义 |
| 检索质量 | Recall@K | Top-K 结果中召回了多少相关文档(最重要!漏召回是 RAG 最大杀手) |
| MRR(Mean Reciprocal Rank) | 第一个相关文档排名的倒数平均(关注排名第一的结果) |
| nDCG | 考虑相关性等级的排序质量(比 Recall 更细粒度) |
| 生成质量 | Faithfulness/Groundedness | 回答是否完全基于检索到的上下文(不编造上下文没有的信息) |
| Answer Relevance | 回答是否解决了用户问题(不答非所问) |
| Context Recall | 回答需要的信息是否都在检索到的上下文中 |
| Context Precision | 检索到的上下文中有多少是真正有用的(noise 比例) |
评估工具
- RAGAS:开源 RAG 评估框架,用 LLM 做评判,支持 faithfulness、relevance 等指标
- TruLens:可观测性 + 评估,追踪每次调用的三元组(query → context → answer)
- LangSmith Evaluation:LangChain 官方评估平台,支持数据集评估和在线监控
RAG 常见陷阱与反模式
| 反模式 | 问题 | 正确做法 |
| 盲目增大 Top-K | 召回越多噪音越多,LLM 被无关信息干扰(lost in the middle) | 先用 Rerank 精排,K 控制在 3-8;需要更多上下文时用 Parent-Document |
| 固定分块大小 | 不同文档类型适合不同分块策略 | 代码按函数/类切,问答按 QA 对切,长文档按语义切,表格单独处理 |
| 只做一次检索 | 复杂问题第一次检索往往不够 | 用 IR-CoT / Self-Ask 多跳检索,检索-推理交替进行 |
| 忽略 Embedding 模型领域适配 | 通用 embedding 在垂直领域(医疗/法律/代码)效果差 | 用领域微调的 embedding 模型;或者用 bge-m3 这类多语言通用强模型 |
| 把所有数据塞进一个向量库 | 不同来源/类型的数据混在一起,检索时跨域噪音大 | 按领域/文档类型分 collection,检索时路由到正确的 collection |
| 不做冗余去重 | Multi-Query/Hybrid 检索返回大量重复内容 | 检索后做去重(按内容相似度或文档 ID),再 Rerank |
Embedding 模型选择指南
Embedding 模型质量直接决定检索上限,选对模型比优化分块策略收益更大。
| 场景 | 推荐模型 | 维度 | 说明 |
| 英文通用 | text-embedding-3-large (OpenAI) | 3072(可降维) | 质量最高,API 调用方便,成本可接受 |
| 中文通用 | bge-m3 (BAAI) | 1024 | 多语言、多功能(稠密+稀疏+ColBERT),开源可本地部署 |
| 本地部署/开源 | bge-large-zh-v1.5, gte-Qwen2 | 1024-1536 | 平衡质量和速度,支持中文 |
| 代码检索 | voyage-code-2, bge-large-code | 1024-1536 | 针对代码训练,理解变量/函数/语法结构 |
| 长文档 | 需要支持长上下文的 embedding | 看模型 | 注意 embedding 模型的最大输入长度(通常 512-8192 tokens) |
实践经验:MTEB 排行榜可以参考但不要迷信——在你的真实数据上做 Recall@K 评测才是最可靠的选择标准。bge-m3 因为同时支持稠密、稀疏(BM25 风格)和 ColBERT 晚交互三种模式,是开源部署的首选。
上下文压缩与窗口管理
即使检索到相关文档,直接全部塞进 prompt 也会有问题:上下文太长会稀释关键信息、增加成本、触发 lost-in-the-middle(LLM 对中间内容注意力弱)。需要做上下文压缩:
| 技术 | 做法 |
| LLM 压缩器 | 用 LLM 阅读每个检索到的 chunk,提取和 query 相关的句子,丢弃无关内容 |
| CRAG (Corrective RAG) | 检索后评估文档相关性:相关→用;不相关→触发 web search 补充;模糊→纠错后使用 |
| Self-RAG | 模型自己判断是否需要检索、检索的文档是否相关、生成内容是否有依据,全程自我批判 |
| 自适应检索 | 简单问题不检索(直接用参数知识),需要事实的问题才检索,减少不必要的检索 |
Q: Parent-Document 和普通 chunking 的区别?
普通 chunking 的困境:chunk 太小(如 256 tokens)检索精确但 LLM 缺乏上下文容易答错;chunk 太大(如 2000 tokens)上下文丰富但检索时噪音多,embedding 稀释语义,精确匹配率低。
Parent-Document retrieval 解决这个矛盾:
- 索引时:文档切为小 chunk(child,用于精确检索),同时保留大 chunk(parent,包含完整上下文)
- 检索时:用小 chunk 做向量搜索,找到最匹配的 child chunk
- 返回时:不返回 child,而是返回 child 对应的 parent chunk 给 LLM
类比:查字典时你通过精确的词条(child)找到页码,但阅读时看整个词条解释的段落(parent),而不是只看单个字。
Parent-Document 用小 chunk 检索保证精度,用大 parent chunk 返回保证上下文完整,解决了分块大小在精度和上下文之间的矛盾。
Q: 为什么 Hybrid Retrieval + Rerank 比纯向量好?
Hybrid(Dense + Sparse)互补:
- Dense retrieval(向量)擅长语义匹配:「怎么提高代码性能」能匹配到「性能优化指南」
- Sparse retrieval(BM25)擅长精确匹配:错误码「ERR_CONN_RESET」、用户 ID、专有名词,向量检索经常漏掉
- RRF 融合不需要调权重,鲁棒性强
Rerank 进一步提升精度:
- Bi-encoder(embedding)是双塔独立编码,query 和 doc 没有交叉注意力,丢失了细粒度交互信号
- Cross-encoder(reranker)把 query 和 doc 拼在一起做全注意力,能捕捉深层语义关系,精度显著更高
- 召回 N=100 再精排到 K=5,兼顾速度和精度
工程经验:纯向量 → Hybrid 通常提升 10-20% 效果;Hybrid + Rerank 再提升 15-30%。这是 RAG 效果优化的「性价比之王」组合。
Dense 补语义、Sparse 补精确关键词(互补)+ Rerank 用 cross-encoder 精排(精度提升最大),三者组合是工业界 RAG 标准做法。
Q: GraphRAG 解决什么问题?
GraphRAG 主要解决两类向量 RAG 解决不好的问题:
1. 全局性/总结性问题:「这份文档集的主要主题是什么?」「A 和 B 之间有什么关系?」这类问题需要聚合多个 chunk 的信息,而向量检索每次只返回局部最相似的 K 个 chunk,无法做全局聚合。GraphRAG 通过社区摘要 + map-reduce 回答。
2. 实体间关系/多跳问题:「X 公司的 CEO 的母校是哪里?」这类跨实体的多跳问题,向量检索可能把 X 公司和 CEO 放在一个 chunk 里,但 CEO 和母校的关系在另一个 chunk 里,需要顺着实体关系链走。知识图谱显式建模实体-关系,支持沿着边遍历。
向量 RAG 仍然更好的场景:局部事实查询(「某 API 参数是什么」)、语义匹配、不需要跨文档聚合的问题。GraphRAG 构建成本高(多次 LLM 调用抽实体建图),不要盲目使用。
GraphRAG 解决全局总结和跨实体关系查询问题,这些是向量 RAG 的盲区;但构建成本高,局部事实查询仍用向量 RAG。
Q: HyDE 为什么有效?
HyDE(Hypothetical Document Embedding)有效的核心原因:embedding 空间中,答案和真实文档的距离比问题和真实文档的距离更近。
直觉解释:
- Query 是「问题空间」的表达,通常短、抽象、用问句形式
- Document(真实答案)是「答案空间」的表达,长、具体、用陈述形式
- Query embedding 和 document embedding 在空间中分布不一致——这叫「embedding space misalignment」
- HyDE 让 LLM 先写一个「假设答案」(即使包含事实错误),这个假设答案在 embedding 空间里和真实文档在同一个区域(都是答案的分布),因此用它检索能找到更相关的真实文档
关键洞察:假设答案不需要事实正确,它只需要在语义风格/词汇分布上接近真实文档,就能拉准检索方向。错误的事实会被真实检索到的文档纠正。
Query 和 Document 在 embedding 空间分布不对齐;HyDE 用零样本生成的假设答案作为「探针」进入答案空间检索,假设答案即使事实错了也没关系,风格相近就能拉准方向。
Q: 怎么评估 RAG 系统质量?
评估 RAG 要分检索层和生成层分别评估,不要只看端到端答案对不对:
离线评估流程:
- 构建评测集:准备 (query, expected_answer, relevant_docs) 三元组(50-200 条)
- 检索层指标:Recall@K(最重要!)、MRR、nDCG。如果 Recall 低,优化分块/embedding/混合检索
- 生成层指标(用 LLM-as-Judge 或人评):Faithfulness(不编造)、Relevance(答对应问)、Context Precision(无噪音)
- 端到端:答案准确率
工具选择:RAGAS 是最常用的开源框架,用 GPT-4 做裁判自动评分;TruLens 和 LangSmith 适合生产环境持续监控。
在线评估:记录用户反馈(点赞/点踩)、追踪「检索到但没用上的上下文」比例、监控幻觉率。
分两层评估:检索看 Recall@K(漏召回是最大杀手),生成看 Faithfulness(幻觉率)和 Relevance(答非所问率);工具用 RAGAS 离线评测,在线用 TruLens/LangSmith 监控。
Function Calling 是什么
Function Calling(Tool Calling)是 LLM 的一种能力:模型在对话过程中自主判断是否需要调用外部工具/函数,并输出结构化的 JSON 参数。开发者负责执行函数,将结果返回给模型,模型再基于工具结果继续推理或生成最终回答。
01
定义工具
开发者向 API 注册工具列表(name, description, parameters JSON Schema)
02
用户查询
发送用户消息,模型判断是否需要调用工具
03
模型决策
模型决定调用哪个工具、传入什么参数,输出 tool_calls
04
执行函数
开发者解析参数,执行本地/远程函数,获取结果
05
返回结果
将执行结果作为 role="tool" 的消息发回模型
06
继续推理
模型读取结果,决定回答用户或继续调用其他工具
消息格式详解(OpenAI 风格)
Function Calling 涉及四种角色的消息,面试要能写出完整的消息流转。
1. System Message — 工具定义
{
"role": "system",
"content": "你是一个助手,可以使用工具获取信息。"
}
// tools 参数单独传入:
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 Beijing、Shanghai"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位"
}
},
"required": ["city"]
}
}
}]
2. Assistant Message — 模型决定调用工具
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"Beijing\", \"unit\": \"celsius\"}"
}
}]
}
3. Tool Message — 开发者返回工具结果
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"temperature\": 25, \"condition\": \"sunny\", \"humidity\": 40}"
}
4. 后续 Assistant Message — 模型基于结果回答
{
"role": "assistant",
"content": "北京今天天气晴朗,气温 25°C,湿度 40%,适合户外活动。"
}
关键约束:tool message 必须通过 tool_call_id 关联到对应的 tool_call,不能凭空发送;arguments 是 JSON 字符串(需要解析),content 也是字符串(建议 JSON 序列化)。
Parallel Tool Calls(并行工具调用)
模型可以在一个 assistant 消息中同时调用多个工具——在 tool_calls 数组中放多个条目,每个有独立的 id。开发者应并行执行所有工具,然后一次性返回所有 tool message。
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_001",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\": \"Beijing\"}"}
},
{
"id": "call_002",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\": \"Shanghai\"}"}
}
]
}
// 返回时发两条 tool message,对应各自的 id
并行调用的好处是减少 RTT(多工具不需要一轮轮串行调用),但前提是工具之间没有依赖关系。如果 tool B 需要 tool A 的结果,模型自然会分轮次调用。
Structured Outputs vs Function Calling
这是两个相关但不同的概念,面试常问区别。
| 维度 | Structured Outputs | Function Calling |
| 目的 | 约束模型输出符合 JSON Schema 的结构化文本 | 让模型决定是否/何时调用外部工具 |
| 是否执行 | 不执行任何操作,只是输出格式约束 | 开发者需要执行函数并返回结果 |
| 输出位置 | assistant.content 是符合 schema 的 JSON | assistant.tool_calls 包含调用信息 |
| 典型场景 | 信息抽取、分类、生成结构化数据 | 搜索、API 调用、数据库查询、代码执行 |
| 工具调用 | 无 | 有,需要 tool message 响应 |
关系:Structured Output = "输出格式约束(JSON mode 的升级版,保证 schema 合规)";Function Calling = "决策 + 格式约束(决定调工具 + 输出参数)"。Function Calling 内部其实用了 Structured Output 能力来保证 arguments 合规。
关键参数控制
| 参数 | 取值 | 作用 |
tool_choice | "auto" | (默认)模型自主决定是否调用工具、调哪个 |
"required" | 模型必须至少调用一个工具(禁止纯文本回答) |
"none" | 模型不调用任何工具,只生成文本(禁用工具) |
{"type": "function", "function": {"name": "xxx"}} | 强制模型调用指定工具(如强制输出结构化数据) |
parallel_tool_calls | true/false | 是否允许一次 assistant 消息中并行调用多个工具(默认 true) |
tool_choice 是最常用的控制参数:当模型应该调工具却没调时(如需要查实时数据却凭记忆回答),用 "required" 强制调用;当工具描述模糊模型乱调时,用具体 function 名强制指定。
最佳实践
| 实践 | 说明 |
| Tool 描述质量 > Prompt 工程 | 工具的 description 是模型选择工具的核心依据。要写清楚:什么时候用、参数含义、enum 值说明、什么时候不要用。描述模糊 = 工具乱调。 |
| 少而精 > 多而模糊 | 10 个描述清晰的工具 >> 50 个描述模糊的工具。工具太多增加选择难度,模型容易选错。 |
| 优雅处理错误 | 工具执行失败时,不要抛异常结束循环;把错误信息作为 tool message 返回(如 "error": "City not found, please check city name"),模型会自动修正参数重试。 |
| 流式工具调用 | 开启 stream 时,arguments 是逐 token 流式输出的,需要累积拼接完整 JSON 后再执行。OpenAI API 通过 deltas 增量返回。 |
| 工具链(Tool Chaining) | 模型自然支持链式调用:调 tool A → 读结果 → 调 tool B → ... → 最终回答。不需要特殊处理,只需持续循环直到没有 tool_calls。 |
| JSON Mode vs 原生 Function Calling | JSON mode 只保证输出合法 JSON,但不保证符合 schema;Structured Outputs / 原生 function calling 能严格保证 schema 合规(字段、类型、enum)。 |
常见问题与陷阱
| 问题 | 原因 | 解决方案 |
| 幻觉参数(hallucinated parameters) | 模型编造 schema 中不存在的参数值或枚举 | 用 Structured Outputs 模式(strict schema);description 中明确列出合法值;错误信息作为 tool message 返回让模型重试 |
| 该调工具不调 | 模型自信地凭记忆回答,不知道信息过时/不可用 | tool_choice: "required";system prompt 中强调「需要实时数据必须调用工具」 |
| 选错工具 | 工具描述不清晰,多个工具功能重叠 | 精简工具数量,每个工具 description 明确区分适用场景 |
| 无限工具调用循环 | 工具返回结果触发模型再次调用同一工具,无终止条件 | 设置最大迭代次数(如 max 10 轮);检测重复调用并终止;在 tool 结果中明确「无更多信息」 |
| 工具参数注入攻击 | 恶意用户输入诱导模型把 prompt 注入内容作为工具参数执行(如让 shell 工具执行 rm -rf) | 所有工具参数严格校验(类型、范围、白名单);高风险操作需要人工确认;永远不要直接把模型输出拼接到 shell/SQL 中 |
代码示例:Python SDK 完整流程
from openai import OpenAI
import json
client = OpenAI()
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
}]
def execute_function(name, args):
if name == "get_weather":
return {"temperature": 25, "condition": "sunny"}
return {"error": f"Unknown function {name}"}
messages = [{"role": "user", "content": "北京今天天气怎么样?"}]
max_iterations = 5
for i in range(max_iterations):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
messages.append(msg)
if not msg.tool_calls:
print("最终回答:", msg.content)
break
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = execute_function(tc.function.name, args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False)
})
Q: Function Calling 是模型训练出来的还是后处理的?
现代模型(GPT-4、Claude 3、Gemini 等)的 Function Calling 能力是通过 SFT(监督微调)训练出来的,不是简单的后处理解析。
训练过程大致是:
- 收集大量(用户查询,工具定义,正确 tool_call)的标注数据
- 用这些数据做 SFT,教模型在什么场景下输出什么格式的 tool_calls
- 部分模型还经过 RLHF 阶段,进一步提升工具选择和参数生成的准确率
模型内部生成的是特殊的 token 序列来标记 tool call 的起止和结构,API 层再解析为结构化 JSON 返回。早期的 function calling(如 gpt-3.5-turbo-0613 刚推出时)确实更像 prompt engineering 的结果,但现在主流模型都把它作为原生训练目标。
主流模型的 Function Calling 是 SFT 训练出来的原生能力,不是后处理正则匹配;模型学会了在对话流中何时输出工具调用标记、如何组织参数结构。
Q: 怎么让模型一定调用某个工具?
使用 tool_choice 参数强制控制:
方式 1:强制必须调至少一个工具(不允许纯文本回答):
tool_choice="required"
方式 2:强制调用指定的某个工具(精确控制):
tool_choice={"type": "function", "function": {"name": "extract_entities"}}
补充手段:
- System prompt 中明确指示:「要回答这个问题,你必须先调用 search 工具获取最新信息」
- 确保工具 description 写清楚什么时候用,不给模型「凭记忆回答」的理由
- 如果模型该调不调,检查是否工具描述太泛(模型认为自己知道答案),可以加上「该信息可能过时,必须调用工具确认」
反面:不要用 tool_choice="none",那会禁用所有工具调用。
用 tool_choice 参数控制:"required" 强制至少调一个,传入具体 function 名强制调指定工具;配合 system prompt 明确指示。
Q: 并行 tool call 怎么处理?
当一个 assistant 消息的 tool_calls 数组包含多个条目时,就是并行工具调用。处理步骤:
- 不要串行等待:解析出所有 tool_calls,每个有独立的
id、name、arguments - 并行执行:用线程池/asyncio.gather 同时执行所有工具函数(如果它们之间没有依赖)
- 分别返回:每个工具结果作为单独的
role: "tool" 消息,通过 tool_call_id 关联到对应调用,一次性全部追加到 messages - 继续循环:发回模型,让模型基于所有结果继续推理
示例:
if msg.tool_calls:
# 并行执行所有工具
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as pool:
futures = {
pool.submit(execute_function, tc.function.name, json.loads(tc.function.arguments)): tc
for tc in msg.tool_calls
}
for future in concurrent.futures.as_completed(futures):
tc = futures[future]
result = future.result()
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False)
})
注意:tool messages 的顺序不重要,关键是 tool_call_id 正确匹配。
并行 tool call 即 tool_calls 数组有多个条目;应并行执行所有无依赖的工具,每个结果对应独立 tool message(通过 tool_call_id 关联),一次性发回模型。
Q: tool message 格式是怎样的?
Tool message 必须满足以下格式要求:
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"result\": \"工具返回的结果字符串\"}"
}
关键字段:
role:必须是字符串 "tool"tool_call_id:必须精确匹配 assistant tool_call 中的 id 字段(如 "call_abc123"),不能省略、不能写错——这是关联请求和响应的唯一依据content:字符串类型,建议 JSON 序列化(因为模型是文本模型,结构化内容要序列化为字符串);也可以是纯文本错误信息
常见错误:
- 忘记传
tool_call_id → API 报错 tool_call_id 不匹配 → 模型不知道这个结果对应哪个调用- content 传 dict 而不是字符串 → 序列化错误
- 多个工具调用只返回一个 tool message → 其他 tool_call 没有响应对
Tool message 三要素:role="tool"、tool_call_id 精确匹配对应调用、content 是字符串(建议 JSON);每个 tool_call 必须有对应的 tool message 响应。
Q: Structured Output 和 Function Calling 区别?
| 维度 | Structured Outputs | Function Calling |
|---|
| 本质 | 输出格式约束(保证 schema 合规) | 决策能力 + 格式约束(是否调工具 + 参数格式) |
| 输出在哪里 | assistant.content(JSON 文本) | assistant.tool_calls(结构化调用信息) |
| 需要开发者执行吗 | 不需要,直接拿结构化内容用 | 需要解析参数执行函数,返回结果 |
| 需要 tool message 吗 | 不需要 | 必须,否则对话无法继续 |
| 典型场景 | 信息抽取、分类打标、提取实体、数据解析 | 查天气、搜文档、调 API、查数据库、执行代码 |
| 类比 | 「让模型填表」 | 「让模型发号施令」 |
联系:Function Calling 的 arguments 生成内部使用了 Structured Outputs 能力(保证参数 JSON 符合 schema)。可以把 Structured Outputs 看作 Function Calling 的「只输出不执行」特例。
如果用 tool_choice 强制调用某个工具,本质上就是用 Function Calling 机制做 Structured Output——模型会输出一个 tool_call,你不需要真的执行函数,直接解析 arguments 当结构化输出用即可。
Structured Output 是格式约束(输出合规 JSON 到 content,不执行),Function Calling 是决策+执行(模型决定调工具,输出到 tool_calls,开发者执行后返回 tool message)。