7附课10 分钟

附07:Agent Tool 调用机制深度 —— LLM 的"手脚"

理解 Tool Calling 底层原理(@Tool → JSON Schema 全链路)、掌握 ReAct 循环完整示例、学会 Tool 设计四原则。

AgentTool CallingReActFunction CallingSelf-RAG
进度保存在本机浏览器;验收通过后再点更稳妥

附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");
    }
}

核心要点

  1. @Tool → JSON Schema → LLM 选择 → 反射调用 → 结果回传——这就是 Tool Calling 全链路
  2. ReAct = Thought → Action → Observation 循环,直到能回答
  3. Tool 描述要啰嗦、原子化、返回值可读、错误要教 LLM 补救
  4. Self-RAG 让 LLM 自己判断"该不该检索、检索结果好不好"

下篇附10+ 讲 LLM 评估体系。在此之前,第8、9课的多租户和可观测性建议先按原节奏学完。