用 Rust 写 LLM Agent:原生工具调用、检索纠错、自适应路由、深度研究、多步规划
为什么一个 Agent 框架要实现 6 种 Agent?为什么不是 1 种通吃的?每种背后的真实痛点是什么?该选哪个?能组合吗?
本文不讲代码,只讲思路。基于 langchainrust v0.8.0 的实践。
先回答一个根本问题:为什么不是 1 种 Agent 通吃?
很多人的第一反应是:设计一个通用 Agent,能调用工具、能检索、能规划,不就完了?
问题在于,"通用"往往意味着"什么都不擅长"。
- 通用 Agent 调用工具,必须处理 FC 和非 FC 两种情况 → 代码路径分叉,每条路径都不够深
- 通用 Agent 兼顾检索,但简单问题不需要检索 → 白白浪费嵌入和向量搜索的 API 调用
- 通用 Agent 要能规划,但简单问答不需要规划 → 多了一次 LLM 调用,多了延迟
所以不是"1 种 Agent 通吃",而是6 种 Agent 各自解决一类特定问题,然后通过组合覆盖所有场景。
这 6 种 Agent 的存在,背后是 5 个真实痛点:
| 痛点 | 哪些 Agent 在解决 |
|---|---|
| LLM 调用工具的方式不统一 | FunctionCallingAgent、ReActAgent |
| 检索结果可能不可靠 | CorrectiveRAG |
| 不是所有问题都需要检索 | AdaptiveRAG |
| 研究型任务无法一次搜索完成 | DeepResearchAgent |
| 多步骤任务可能中途失败 | PlanExecuteAgent |
下面逐个讲透。
统一抽象:Plan-Act-Observe 循环
在讲 6 种之前,先说清楚它们共享的东西。
所有 Agent 的执行模式都是同一个循环:
用户输入 → [规划下一步] → [执行工具] → [观察结果] → 还有动作?→ 重复 → 输出最终答案
这就是 Plan-Act-Observe 循环,来自经典的 ReAct 论文。6 种 Agent 的差异不在于循环本身,而在于三个关键决策点:
- 规划怎么做 --- 原生 Function Calling?文本解析?检索+评分?LLM 分解步骤?
- 执行什么 --- 单工具?并行工具?向量检索?Web 搜索?
- 何时结束 --- LLM 说完了?文档评分够了?信息没有缺口了?所有步骤都完成了?
理解了这三个决策点,6 种 Agent 的区别就一目了然了。
一、FunctionCallingAgent --- 原生工具调用
LLM 支持 Function Calling 时,这是最简单最可靠的方案。你把工具的 JSON Schema 告诉 LLM,LLM 需要调用工具时直接返回结构化的 tool_calls 字段。不需要从文本里"猜"LLM 想调用什么工具。
可靠性 。文本解析永远有失败率 --- LLM 可能输出 Action: calculator(123*456) 而不是你期望的格式。但 tool_calls 是 Provider 保证的结构化字段,不会格式错误。
并行性 。LLM 一次返回多个 tool_calls 时,所有工具可以并行执行,不需要串行等待。
能力探测 。不是所有 Provider 都支持 FC。bind_tools 返回 Option --- OpenAI 支持→走原生 FC;Ollama 不支持→回退到普通 chat。这是 trait 设计时就预留的语义:"Provider 可能不支持工具绑定"是一个合法状态,不是异常。
适用场景 :OpenAI、Anthropic、Gemini 等支持 Function Calling 的模型。只要能用 FC,就用这个,没有例外。
二、ReActAgent --- 文本推理兜底
LLM 不支持 Function Calling 时(比如某些开源模型、Ollama 部分模型),用文本 prompt 引导 LLM 输出固定格式,再用解析器提取:
Thought: 我需要查一下北京的天气
Action: weather[北京]
Observation: 晴天,25°C
Thought: 我现在知道答案了
Final Answer: 北京今天是晴天,25°C
代价:解析脆弱。LLM 可能输出格式不对的文本,解析器永远无法 100% 覆盖所有变体。
收益:推理透明。每个 Thought 步骤都可观察,调试时能看到 LLM "在想什么"。FunctionCallingAgent 里这些推理过程是隐藏的。
| 维度 | FunctionCallingAgent | ReActAgent |
|---|---|---|
| 工具调用方式 | LLM 原生结构化字段 | 文本 prompt + 解析器 |
| 可靠性 | 接近 100% | 有解析失败率 |
| 推理可见性 | 隐藏 | 每个 Thought 都可观察 |
| 适用范围 | 仅支持 FC 的模型 | 任何 chat model |
适用场景 :不支持 FC 的模型,或需要观察推理过程的调试场景。能用 FC 就用 FC,不能用才用 ReAct。
三、CorrectiveRAG --- 检索纠错
传统 RAG 有一个危险的假设:检索到的文档一定有用。但现实中向量搜索经常返回完全不相关的文档。如果用垃圾文档生成答案,结果就是"看似有据,实则胡说" --- 这比直接让 LLM 回答更危险,因为用户会以为答案是有依据的。
举个例子:用户问"Rust 的所有权机制",检索到了一篇讲 Rust 真菌防治的农业论文(向量相似度碰巧很高)。传统 RAG 会把这篇论文塞给 LLM 当上下文,LLM 可能基于它生成一个似是而非的答案。
CRAG 在检索和生成之间加了一层评分:检索到的文档先让 LLM 打分,分数够才用,分数不够就纠错。
检索 → 评分 → 分数够?
├── 够 → 直接生成答案
└── 不够 → 重写查询 → Web 搜索兜底 → 重新检索 → 生成答案
└── 幻觉检查 → 答案是否真的有据可依?
评分阈值 。默认 0.6,可调 --- 医疗法律严格场景调到 0.8,闲聊场景降到 0.4。本质是对检索质量的信心和纠错成本之间的权衡。
Web 搜索兜底。本地知识库没有的内容,可以通过 DuckDuckGo 等搜索引擎补充。解决了一个常见问题:用户问的东西根本不在你的文档库里。
grader_llm 避免自我偏好。用同一个 LLM 评分和生成,它倾向于认可自己的输出。用独立的 grader_llm 做幻觉检查,打破这个偏见。这不是技术问题,是认知科学问题 --- 人不善于审查自己的判断,LLM 也一样。
CRAG 的返回值比普通 RAG 丰富得多:不只是答案,还有 grounded(是否有据可依)、grade_scores(文档评分)、grade_reasoning(评分理由)。grounded: false 比一个错误的答案更有价值 --- 它诚实地告诉你"这个答案可能不可靠"。
适用场景 :检索质量不稳定、文档库可能不完整、需要答案"有据可依"的场景。任何需要引用溯源的场景(医疗、法律、金融)都应该用 CRAG,而不是普通 RAG。
四、AdaptiveRAG --- 自适应检索路由
传统 RAG 的另一个假设:所有问题都需要检索。但"法国首都"这种常识问题,LLM 直接回答就好,检索是浪费时间和金钱。反过来,"2024年诺贝尔物理学奖授予了谁"这种时效性问题,必须检索。
AdaptiveRAG 在检索之前加了一层路由:让 LLM 先判断问题的性质,再决定用什么策略。
| 决策 | 含义 | 动作 | 省了什么 |
|---|---|---|---|
| NoRetrieval | 常识问题,LLM 直接回答 | 跳过检索 | 嵌入 + 向量搜索 |
| SingleSearch | 需要检索,一次足够 | 标准检索 + 生成 | 无 |
| MultiQuery | 复杂问题,需要多角度 | 生成多个查询变体 → 逐个检索 → 合并 | 避免单次检索遗漏 |
算一笔账。假设 100 个查询中 30 个常识问题、50 个需要单次检索、20 个需要多查询。对比"无脑全检索":省了 30 次嵌入 + 30 次向量搜索。如果每次嵌入+搜索耗时 200ms,那就是省了 6 秒总延迟和对应的 API 费用。在大规模生产环境中,这个差异是显著的。
与 CRAG 的关系:AdaptiveRAG 解决"要不要检索、检索几次",CRAG 解决"检索结果可不可靠"。两者可以组合 --- AdaptiveRAG 决定检索策略,CRAG 确保检索质量。就像去医院:AdaptiveRAG 是分诊台(决定你去哪个科室),CRAG 是主治医生(确保诊断准确)。
适用场景 :查询量大、混合了简单问题和复杂问题的场景。特别是成本敏感的生产环境。
五、DeepResearchAgent --- 深度研究
"帮我调研 AI 对医疗行业的影响" --- 这种研究型问题,无法通过一次搜索完成。一个研究主题通常包含 3-5 个子主题,每个子主题需要单独搜索,搜索结果可能有信息缺口需要补充搜索,最终要综合成一篇有引用的报告。传统 RAG 完全不够用 --- 它只能做"一次检索 + 一次生成"。
DeepResearch 把研究过程拆成四个阶段:
1. Planner --- 分解子主题
LLM 把研究主题分解为 3-5 个子主题,每个子主题生成搜索查询。比如"AI 对医疗的影响"分解为:AI 在医疗影像中的应用、AI 在药物研发中的应用、AI 在临床决策中的应用。
2. Searcher --- 并行搜索
所有子主题的查询并行搜索,不是串行等待。多个搜索工具(DuckDuckGo、Wikipedia 等)也可以并行查询。搜索结果自动去重。
3. Synthesizer --- 综合报告 + 引用
把所有搜索结果综合成 Markdown 报告,每个论点带引用标记 [1][2][3],末尾有完整的 Citation 列表(来源、URL、片段)。
4. 信息缺口检测 --- 动态补充
综合报告后,LLM 检查是否有信息缺口。比如"子主题 X 的搜索结果只有 1 条,不足以支撑结论"。有缺口才生成补充查询继续搜索,没有缺口提前结束。很多实现是固定搜索 3 轮或 5 轮,但这是浪费 --- 简单主题 1 轮就够了,复杂主题可能需要 5 轮。信息缺口检测替代固定轮数,简单主题不会过度搜索,复杂主题不会被截断。
| 维度 | 普通 RAG | DeepResearch |
|---|---|---|
| 检索次数 | 1 次 | 多轮,按需补充 |
| 查询数量 | 1 个 | 子主题分解后 3-15 个 |
| 信息缺口 | 不检测 | 自动检测并补充 |
| 引用溯源 | 无 | 每个论点带引用 |
| 输出格式 | 一段文本 | Markdown 报告 + Citation 列表 |
本质区别:普通 RAG 回答问题 ,DeepResearch 做研究。
适用场景 :深度研究型任务:竞品分析、技术调研、文献综述、投资研报。任何需要全面、有引用、有深度的回答。
六、PlanExecuteAgent --- 多步规划与重规划
"帮我调研最新的 Rust 框架并做对比表" --- 这种任务不是一次工具调用能完成的。需要先规划步骤(搜索框架 → 对比特性 → 生成表格),逐步执行,中途某步可能失败 --- 搜索超时、结果不够、对比维度遗漏。失败后不应该放弃整个任务,而是重新规划。
PlanExecute 把任务执行分为两层:
编排层:LLM 做规划,分解步骤,监控执行,失败时重规划。
执行层:每个步骤内部又是一个完整的 Agent 循环(用 FunctionCallingAgent 驱动),可以调用工具完成单步任务。
PlanExecuteAgent(编排层:决定做什么)
└── 步骤 1:搜索 Rust Web 框架 → FunctionCallingAgent 执行
└── 步骤 2:搜索 Rust CLI 框架 → FunctionCallingAgent 执行
└── 步骤 3:对比特性 → FunctionCallingAgent 执行
└── 步骤 3(失败:信息不足)→ Replanner 生成新计划
└── 步骤 3':搜索更多框架 → FunctionCallingAgent 执行
└── 步骤 4:生成对比表 → FunctionCallingAgent 执行
增量重规划。失败时不是从头开始,而是告诉 Planner 哪步失败了、为什么失败,Planner 只修改失败步骤及后续步骤,已完成的步骤保留。重规划次数有上限(默认 2 次),防止无限循环 --- 两次重规划都搞不定,说明任务超出了 LLM 的能力范围,应该返回错误而不是继续烧 token。
与 DeepResearch 的区别(最容易混淆的两个):
| 维度 | DeepResearchAgent | PlanExecuteAgent |
|---|---|---|
| 专注领域 | 研究:搜索 + 综合 | 通用:任意多步骤任务 |
| 步骤类型 | 固定三阶段:plan → search → synthesize | 动态:LLM 自由规划 |
| 失败处理 | 信息缺口 → 补充搜索 | 步骤失败 → 重新规划 |
| 输出 | 研究报告 + 引用 | 步骤结果汇总 |
| 工具 | 搜索工具 | 任意工具 |
DeepResearch 是特化的 --- 步骤类型固定为 plan/search/synthesize,专门优化了研究流程。PlanExecute 是通用的 --- 步骤由 LLM 动态规划,可以是搜索、分析、生成、调用 API、发送邮件,任何操作。
选哪个? 任务就是"研究某个主题"→ DeepResearch,更专业。任务混合多种操作(搜索 + 分析 + 生成 + 发送)→ PlanExecute,更灵活。
适用场景 :多步骤复杂任务:调研 + 分析 + 生成报告、数据收集 + 清洗 + 分析、多轮迭代优化。任何步骤不确定、可能中途失败的任务。
选择指南
| 你的场景 | 推荐 | 核心原因 |
|---|---|---|
| LLM 支持 FC,需要调用工具 | FunctionCallingAgent | 最可靠,结构化输出 |
| LLM 不支持 FC | ReActAgent | 文本解析兜底,推理透明 |
| 检索结果可能不可靠 | CorrectiveRAG | 评分 + 纠错 + 幻觉检查 |
| 不确定是否需要检索 | AdaptiveRAG | 自适应路由,省成本 |
| 需要深度研究报告 | DeepResearchAgent | 多轮搜索 + 引用 |
| 多步骤复杂任务 | PlanExecuteAgent | 规划 + 执行 + 重规划 |
组合使用
6 种 Agent 不是互斥的,可以嵌套组合:
- PlanExecuteAgent 的每步执行 → 嵌套 FunctionCallingAgent(编排 + 执行分层)
- DeepResearchAgent 的检索策略 → 可以用 AdaptiveRAG 路由(省掉不必要的检索)
- AdaptiveRAG 的检索质量 → 可以加 CRAG 的评分纠错(检索了,但结果不可靠怎么办)
Agent 的本质是策略,策略可以组合。
进化路径
从简单到复杂,推荐这个顺序接入:
1. 先用 FunctionCallingAgent 跑通基本工具调用
2. 加 ReActAgent 兼容更多模型
3. 加 CorrectiveRAG 保证检索质量
4. 加 AdaptiveRAG 优化成本
5. 加 DeepResearchAgent 支持研究场景
6. 加 PlanExecuteAgent 支持复杂任务
每一步都是增量,不需要推翻前面的。
统一 trait 的边界
FunctionCallingAgent 和 ReActAgent 实现了 BaseAgent trait,可以被 AgentExecutor 统一驱动。但 CRAG、AdaptiveRAG、DeepResearchAgent 没有实现这个 trait。
为什么?因为它们的返回值有特殊语义:
- CRAG 返回
grounded: bool--- 答案是否有据可依 - AdaptiveRAG 返回
decision: RagDecision--- 路由到了哪条路径 - DeepResearch 返回
citations: Vec<Citation>--- 引用列表
强行塞进 BaseAgent 的 AgentFinish(只有 output 和 log)会丢失这些信息。类型系统在这里帮我们做了正确的事 --- 当输出有特殊语义时,不统一反而更好。统一是有代价的,代价就是丢失信息。
判断标准:如果返回值只是"一段文本",实现 BaseAgent;如果返回值有结构化语义,不实现,暴露自己的接口。
项目
github.com/atliliw/langchainrust --- Rust 实现的 LLM 应用框架