附07:Agent Tool 调用机制深度 —— LLM 的"手脚"
本节目标:理解 Tool Calling 的底层原理(从 @Tool 注解到 JSON Schema 的全链路)、掌握 ReAct 循环的完整决策示例、学会工具设计的四原则、了解 Self-RAG 反思机制。
第3课和第7课分别讲了 Agent 循环和 LangGraph 编排。这一节把 Tool Calling 的底层机制讲透。
1. Tool Calling 的本质
核心问题:LLM 只会"生成文本",怎么让它"做事"?
解决方案:约定一种文本格式,LLM 输出这种格式 = 请求调用工具。
演化史
Code
旧时代(手工解析):
LLM 输出 "ACTION: search_classics PARAMS: {query: '失眠'}"
→ 需要正则解析,脆弱
Function Calling 时代:
LLM 输出 {
"tool_calls": [{
"function": "search_classics",
"arguments": {"query": "失眠 不寐"}
}]
}
→ 结构化 JSON,可靠
LangChain4j @Tool 注解全链路
java
@Tool("在向量知识库中检索与查询最相关的古籍片段。" +
"适用于:需要查找古籍原文、查证医学概念。" +
"参数 query 为核心检索词,建议 2-10 字,topK 建议 3-10。")
public String searchClassicChunks(String query, Integer topK) {
// ...
}
框架在背后做了什么?
code
1. 启动时扫描所有 @Tool 注解的方法
2. 提取信息构建 JSON Schema:
{
"name": "searchClassicChunks",
"description": "在向量知识库中检索...",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"topK": {"type": "integer"}
},
"required": ["query"]
}
}
3. 把所有 Schema 拼进 Prompt 发给 LLM
4. LLM 返回 tool_calls 时,框架自动用反射调用对应方法
5. 把返回值塞回对话历史,再发给 LLM 继续推理
2. 完整 ReAct 循环示例
ReAct = Reason + Act。模式很简单:
code
Loop:
1. Thought: 我现在该做什么?
2. Action: 调用 some_tool(args)
3. Observation: 工具返回了什么?
4. 回到 1,直到 LLM 说"我能回答了"
本项目真实示例
code
User: 黄帝内经里关于失眠怎么说?
═══════ Iteration 1 ═══════
Thought: 用户问失眠,这是口语化术语。古籍中可能叫"不寐"。
我需要先改写查询。
Action: rewriteQuery
Input: "失眠"
Obs: "失眠 不寐 安神 治法"
═══════ Iteration 2 ═══════
Thought: 现在用改写后的查询检索古籍。
Action: searchClassicChunks
Input: {"query": "不寐 失眠 安神", "topK": 5}
Obs: "[1]《素问·逆调论》:胃不和则卧不安...
[2]《灵枢·邪客》:今厥气客于五脏六腑...
[3]《金匮要略》:虚劳虚烦不得眠,酸枣仁汤主之..."
═══════ Iteration 3 ═══════
Thought: 已获得3条相关证据,判断是否足够。
Action: judgeNeedMoreEvidence
Input: {"question": "...", "chunkIds": [1,2,3]}
Obs: false (证据充分)
═══════ Iteration 4: Final ═══════
Final Answer:
根据《黄帝内经》记载,失眠在中医称为"不寐"...
[1]《素问·逆调论》认为"胃不和则卧不安"...
[2]《灵枢·邪客》提出邪气干扰营卫...
[3]《金匮要略》给出经典方剂酸枣仁汤...
3. 设计 Tool 的四项原则
① 描述要"啰嗦"(最重要)
java
// 坏例子(LLM 不知道何时用)
@Tool("搜索")
public String search(String q) { }
// 好例子
@Tool("在向量知识库中检索与查询最相关的古籍片段。" +
"适用于:需要查找古籍原文、查证医学概念、寻找方剂依据。" +
"不适用于:闲聊、计算、查询用户信息。")
public String searchClassicChunks(String query, Integer topK) { }
② 工具要"原子化"
code
一个工具搞定一切:searchAndRerankAndFormat()
拆成原子工具:search() + rerank() + format()
让 LLM 自己组合,更灵活
③ 返回值要"可读"
Code
// 返回对象 → LLM 看不懂
return chunk;
// 返回结构化文本 → LLM 易理解
return String.format("[%d] 《%s》%s: %s",
chunk.getId(), chunk.getBookTitle(),
chunk.getChapterTitle(), chunk.getOriginalText());
④ 错误处理要"教 LLM 怎么办"
java
if (chunks.isEmpty()) {
return "未找到相关古籍片段,建议尝试同义词或扩大检索范围";
// ↑ 告诉 LLM 该怎么补救,而不是抛异常
}
Tool 数量的天花板
| 工具数 | 选择准确率 |
|---|---|
| 1-5 个 | >95% |
| 5-15 个 | 80-90% |
| 15-30 个 | 60-70% |
| 30+ 个 | 必须用工具检索(Tool Retrieval) |
4. Self-RAG:让 RAG"学会反思"
传统 RAG 的缺陷
Code
Q1: "1+1=?" → 不需要检索却也检索
Q2: "黄帝内经怎么说?" → 检索结果质量差也硬答
Q3: "今天天气怎么样?" → 知识库没有但还是瞎编
Self-RAG 核心创新
引入 4 类反思 token,让 LLM 自己决策:
| Token | 作用 | 取值 |
|---|---|---|
| [Retrieve] | 判断是否需要检索 | Y / N / Continue |
| [IsRel] | 判断检索结果是否相关 | Relevant / Irrelevant |
| [IsSup] | 判断生成内容是否被支持 | Fully / Partially / No |
| [IsUse] | 判断回答是否有用 | 1-5 分 |
简化版实现(不微调,用大模型 in-context)
java
public class SelfRagService {
// Step 1: 判断是否需要检索
public boolean needRetrieval(String question) {
String prompt = """
判断以下问题是否需要查询知识库才能回答。
常识/计算/闲聊回答"NO",需要专业知识回答"YES"。
问题:%s
只回复 YES 或 NO。
""".formatted(question);
return llm.generate(prompt).trim().equalsIgnoreCase("YES");
}
// Step 2: 过滤无关 chunks
public List<ChunkVO> filterRelevant(String question, List<ChunkVO> chunks) {
return chunks.parallelStream()
.filter(c -> isRelevant(question, c))
.collect(Collectors.toList());
}
// Step 3: 验证回答是否被证据支持
public boolean isSupported(String answer, List<ChunkVO> evidence) {
String prompt = """
回答中的每个论点是否都被证据支持?回复 YES/PARTIAL/NO。
""".formatted(answer, format(evidence));
return llm.generate(prompt).trim().equalsIgnoreCase("YES");
}
}
核心要点
- @Tool → JSON Schema → LLM 选择 → 反射调用 → 结果回传——这就是 Tool Calling 全链路
- ReAct = Thought → Action → Observation 循环,直到能回答
- Tool 描述要啰嗦、原子化、返回值可读、错误要教 LLM 补救
- Self-RAG 让 LLM 自己判断"该不该检索、检索结果好不好"
下篇附10+ 讲 LLM 评估体系。在此之前,第8、9课的多租户和可观测性建议先按原节奏学完。