跳到正文

第 3 章:工具集设计,让 Agent 真正「会用工具」

能为调研型 Agent 设计一套工具集(search / visit / scholar / 计算),写出健壮的工具注册与调用,并讲清统一工具网关、容错、缓存怎么做

📊 学习时长:55-70 分钟 🎯 完成后能力:能为调研型 Agent 设计一套工具集(search / visit / scholar / 计算),写出健壮的工具注册与调用,并讲清统一工具网关、容错、缓存怎么做 🔗 关联面试题:5 道(覆盖 5 个角度,见 Part 3)

这一章你会学到什么

  • ✓ 跟着做:给上一章的 mini-agent 配齐一套工具(search + visit + 计算器),亲眼看模型按需求自己挑工具
  • ✓ 看懂一个工具到底由什么组成:名字 + 给模型看的「说明书」+ 参数约束,以及模型怎么知道「有哪些工具可用」
  • ✓ 掌握好工具的设计原则:单一职责、描述清晰、参数受约束、错误可读
  • ✓ 认全调研 Agent 的核心工具:Search / Visit / Scholar / Python 各管一摊
  • ✓ 学会三件工程化的事:统一工具网关、工具容错、多层缓存,这些决定了 Agent 在生产里稳不稳、贵不贵
  • ✓ 想清一个取舍:function calling(结构化)vs 文本标签解析(ReAct 那种),什么时候用哪个

本章按三段式组织:

段落 给谁看 内容 占比
🚀 Part 1 · 主线实战 零基础,想先跑起来 工具的组成 + 给 mini-agent 配齐工具集 ~55%
🎯 Part 2 · 面试深度 想讲透、备战面试 核心工具 + 统一网关 + 容错 + 缓存 + 取舍 + 私货 ~35%
🏆 Part 3 · 验收串题 检验学到位没 关联面试题 + 自检清单 ~10%

🚀 Part 1 · 主线实战 | ~55% 这部分跟着做。你会把上一章那个「只会搜索」的 Agent,升级成一个「会搜、会点进去看、还会算数」的调研助手。

先讲个真实场景(这是本章的「为什么」)

上一章的 mini-agent 只有一个 search 工具。它能拿到的,只是搜索结果的标题和摘要,就像你在搜索引擎里只看到那一行行蓝色链接和下面两句话。

可真正做选型调研,光看摘要远远不够。工程师问:「Qdrant 单机大概能扛多少 QPS?」,这个数字往往不在摘要里,得点进官方文档 / benchmark、读正文才有。再问:「我每天 100 万次查询,平均每秒多少,它扛得住吗?」,这又得算一下

一句话:只有一个工具的 Agent,能力天花板很低。 调研型 Agent 需要一组工具,搜索拿线索、访问拿细节、计算器做数值、学术库查论文 …… 让模型像工程师用浏览器、计算器、文档站一样,按需调用。

这一章,就是讲怎么把这组工具设计好、接好、还能在生产里稳定运行。

概念 1:一个「工具」到底由什么组成

回忆上一章:模型本身不能执行,它只能「说」要调用哪个工具。所以一个工具,要让模型「会用」,必须由三部分组成:

  • 名字(name):search / visit / calc,模型用它来指定调哪个
  • 说明书(description):一句话讲清「这个工具干什么、什么时候用」,这是最关键的,模型完全靠它来判断该不该用、怎么用
  • 参数(parameters):工具接受什么输入,最好带类型和约束(比如 query 是字符串、top_k 是 1-10 的整数)

工业界常把这三部分写成一份 JSON Schema(function calling 的标准格式),随请求一起发给模型。

概念 2:模型怎么知道「有哪些工具可用」?

很多人第一次会困惑:模型又没「装」工具,它怎么知道能调 search?

答案很朴素:你把所有工具的「说明书」(schema)一起塞进给模型的输入里。模型读完就知道「哦,我有这几个工具、各自干啥」,然后在需要时输出一个「我要调用 search,参数是 X」的指令。

所以工具的 description 写得好不好,直接决定模型用得对不对,这是后面「设计原则」要重点讲的。

🛠️ 跟着做:给 mini-agent 配齐工具集

我们在上一章 ReAct 循环的基础上,加一个工具注册表:每个工具 = 一个函数 + 一段说明书,循环里统一分发。新建 tool_agent.py:

# pip install openai
import os, re
from openai import OpenAI

client = OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com")

# ── 三个工具:每个 = 一个函数 + 一段给模型看的「说明书」(真实系统接真 API)──
def search(q):                                   # 搜索:只给线索(标题/摘要)
    KB = {"qdrant": "Qdrant[标题] — Rust 向量库,单机基准约数千 QPS 量级",
          "pgvector": "pgvector[标题] — PostgreSQL 扩展,复用现有库"}
    return next((v for k, v in KB.items() if k in q.lower()), "无相关结果")

def visit(q):                                    # 访问:给正文细节
    PAGES = {"qdrant": "正文:768 维、带过滤的典型配置下,单机 QPS 约 2000~5000;HNSW 索引,Docker 容器即起",
             "pgvector": "正文:支持 HNSW / IVFFlat,受 PostgreSQL 单机资源约束"}
    return next((v for k, v in PAGES.items() if k in q.lower()), "页面无相关内容")

def calc(expr):                                  # 计算:做数值
    try:
        return str(eval(expr, {"__builtins__": {}}))   # 教学版;真实系统要沙箱
    except Exception as e:
        return f"计算出错:{e}"

# 工具注册表:名字 → (函数, 说明书)。说明书决定模型用得对不对
TOOLS = {
    "search": (search, "搜索引擎,输入关键词,返回标题和摘要(只给线索,不含正文)"),
    "visit":  (visit,  "打开某候选的文档,返回正文细节(在 search 拿到线索后再用)"),
    "calc":   (calc,   "计算器,输入一个算术表达式(如 1000000/86400),返回结果"),
}

SYS = "你是技术选型调研助手。可用工具:\n" + \
      "\n".join(f'- {n}("参数"):{d}' for n, (_, d) in TOOLS.items()) + """
每步只输出一个动作:
- 调工具:  Action: 工具名("参数")
- 给答案:  Final: <答案>
先 search 拿线索,要细节再 visit,要算数用 calc。"""

def run(question, max_steps=6):
    msgs = [{"role": "system", "content": SYS}, {"role": "user", "content": question}]
    for step in range(1, max_steps + 1):
        out = client.chat.completions.create(model="deepseek-chat", messages=msgs).choices[0].message.content
        print(f"\n[第 {step} 步] {out.strip()}")
        if out.strip().startswith("Final"):
            print("\n✅ 最终答案:", out.split("Final:")[-1].strip()); return
        m = re.search(r'(\w+)\("(.+?)"\)', out)              # 解析出 工具名 + 参数
        if not m:
            print("(没解析到动作,结束)"); return
        tool, arg = m.group(1), m.group(2)
        obs = TOOLS[tool][0](/projects/deep-research) if tool in TOOLS else f"❌ 没有名为 {tool} 的工具"   # 容错:未知工具
        print(f"   🔧 {tool}(\"{arg}\") → {obs}")
        msgs += [{"role": "assistant", "content": out},
                 {"role": "user", "content": f"Observation: {obs}"}]

run("Qdrant 单机大概能扛多少 QPS?我每天 100 万次查询,平均每秒多少、它扛得住吗?")

运行

export DEEPSEEK_API_KEY=你的key
python tool_agent.py

你应该看到(LLM 输出每次措辞略有不同,但会看到它自己挑工具:先 search 拿线索 → visit 拿 QPS 细节 → calc 算每秒查询量):

[第 1 步] Action: search("Qdrant QPS")
   🔧 search("Qdrant QPS") → Qdrant[标题] — Rust 实现的向量库,单机性能强
[第 2 步] Action: visit("qdrant")
   🔧 visit("qdrant") → 正文:768 维、带过滤的典型配置下,单机 QPS 约 2000&#126;5000;HNSW 索引,Docker 容器即起
[第 3 步] Action: calc("1000000/86400")
   🔧 calc("1000000/86400") → 11.574074074074074
[第 4 步] Final: 你日均 100 万次查询 ≈ 平均每秒 ~12 QPS,远低于 Qdrant 单机的数千 QPS,完全扛得住…

🐛 跑不通看这里

  • 模型不用 visit、只 search 就答?多半是 visit 的 description 写得不够清楚,把「在 search 拿到线索后再用」「返回正文细节」再强调一下。工具用不对,九成是 description 的锅。
  • 模型调了不存在的工具?正常,代码里 ❌ 没有名为 X 的工具 这条容错会把错误喂回去,模型通常下一步会纠正。这正是「工具容错」的雏形(Part 2 细讲)。
  • calc 报错?教学版用了 eval,只为演示;真实系统绝不能直接 eval 用户/模型输入,要用沙箱(见下方说明)。

🛠️ 动手实验:改一句 description,看模型选择变化

visit 的说明书从「打开某候选的文档,返回正文细节」改成夸张含糊的「万能信息获取器,需要任何信息都能用它」,再跑。你大概率会看到模型第一步就乱用 visit 去当搜索使、或者该 search 时不 search这就是「工具描述」的威力,它不是注释,是模型选工具的唯一依据。改一个字,行为就变。

📌 说清楚:这是教学最小实现。三处简化:① 工具是本地 stub(真实接搜索 / 网页 / 沙箱执行 API);② 用正则解析动作(真实用 function calling 的结构化 tool_call,更健壮);③ calceval(真实必须沙箱)。机制是真的,工程化是 Part 2 和后面章节的事。

🧭 你刚才做的,等于真实系统的什么?

  • 你的 TOOLS 注册表 = 真实系统的工具注册 + 分发;
  • 你给每个工具写的说明书 = 发给模型的 function calling schema;
  • 那条 ❌ 没有名为 X 的工具 = 工具容错的最小雏形;
  • 你让模型「按需挑工具」= Agent 的核心能力之一,而「模型挑得准不准」最终要靠训练(后面章节)。

🎯 Part 2 · 面试深度 | ~35% 这部分讲生产级工具系统:核心工具怎么分工、统一网关 / 容错 / 缓存怎么做,以及 function calling 和文本解析的取舍。

2.1 调研 Agent 的核心工具集

不同任务工具不同,但调研型 Agent 通常这四件套:

工具 干什么 关键点
Search 调搜索引擎,返回相关网页列表(标题 + 摘要) 只给线索,不给全文;控制返回数量
Visit 访问某个网页,提取与目标相关的内容 不是整页搬回来,要按当前问题抽取(否则又污染上下文)
Scholar 搜学术文献,拿论文元数据 调研类问题常要权威来源
Python 执行代码,做数值计算 / 数据处理 必须沙箱隔离,限制可用模块和资源

业内行话:Visit 工具最容易被新手做坏,直接把整页 HTML 转文本塞回去。正确做法是 Visit 时就带着「当前要找什么」做相关性抽取,只把相关段落带回来。否则一篇长文档就能把上下文撑爆、把注意力带偏,这又回到了第 2 章那个上下文问题。工具的输出质量,直接影响 Agent 的上下文健康。

2.2 设计原则:好工具长什么样

  • 单一职责:一个工具只干一件事。别做「万能工具」,模型会选困难。
  • 描述清晰:description 是模型选工具的唯一依据,讲清「干什么、什么时候用、什么时候别用」。
  • 参数受约束:用 schema 约束类型和范围(top_k 是 1-10 的整数),减少模型乱传参。
  • 错误可读:工具失败时,返回一句模型能看懂、能据此纠正的错误信息(「页面 404,换个关键词」),而不是抛一个堆栈。

2.3 统一工具网关(生产必备)

工具一多,如果每个都各自接 API、各自鉴权、各自打日志,很快就乱了。生产里会套一层统一工具网关:

  • 统一入口:Agent 只管说「调 search」,网关负责路由到真实服务
  • 统一鉴权 / 限流:API key、配额、QPS 都在网关管,别散在各处
  • 统一日志 / 监控:每次工具调用都记下来(谁调的、耗时、成功失败),排查问题全靠它
  • 统一缓存:见下一节

业内行话:统一工具网关的价值,面试时一句话点透:「让 Agent 和工具解耦,Agent 只认抽象的工具名,底层换搜索厂商、加限流、接缓存,Agent 代码一行不用动。」这是典型的工程分层思维。

2.4 工具容错:别让一次失败毁掉整轮调研

第 1 章就说过,多步 Agent 最怕「错误累积」。工具层是第一道防线:

  • 失败重试 + 降级:搜索超时?重试几次;还不行,降级到备用搜索源,或返回「暂时查不到」让模型换策略
  • 超时控制:每个工具调用都要有超时,别让一个卡死的网页拖垮整轮
  • 重复检测:模型有时会反复搜同一个词(陷在原地),检测到就提示它「这个查过了,换个角度」
  • 参数校验:调用前先校验参数合法(top_k 不能是负数),不合法直接返回可读错误

2.5 缓存策略:省钱又提速

调研类任务里,同样的搜索 / 网页访问会反复出现(不同问题、不同轮次撞到同一来源)。多层缓存能大幅降本提速:

  • 搜索结果缓存:相同 query 短期内直接返回缓存,不打真实搜索 API
  • 网页内容缓存:访问过的页面正文缓存起来,重复 visit 直接取
  • 注意时效:调研类对「新鲜度」敏感,缓存要设合理 TTL,别拿三个月前的缓存答「最新进展」

讲师踩坑:我们早期给搜索结果设了 24 小时缓存,省了不少钱。结果有用户问「某向量库今天刚发布的新版本有什么特性」,系统拿了昨天的缓存,答了旧版本的信息。缓存和时效是一对天生的矛盾,后来我们按「问题是否含时效词(今天 / 最新 / 刚发布)」动态决定要不要走缓存。这种「按场景动态调度」的思路,和第 1 章的分级路由、RAG 系列的分类器前置,是同一种工程审美。

2.6 一个取舍:function calling vs 文本标签解析

最后一个高频考点:工具调用,到底用结构化的 function calling,还是 ReAct 那种文本标签 + 正则解析?

  • function calling(模型原生支持):模型直接输出结构化的 tool_call(JSON),程序好解析、不易出错。生产首选(如果模型支持)。
  • 文本标签 + 正则(ReAct 那种):自己定 Action: search("...") 格式、用正则解析。好处是任何模型都能用、格式完全可控;代价是解析脆弱(模型偶尔写歪格式就崩)。

实务里:模型支持 function calling 就用它(健壮);需要兼容老模型 / 完全掌控格式时,才用文本解析。本章 demo 为了通用 + 让你看清机制,用的是文本解析。

🧭 这一章到这儿:工具集的设计、接入、工程化(网关 / 容错 / 缓存)都讲透了,你能照着设计得出、接得通、跑得起来。但还有个根本问题没解决:怎么让模型本身「更会用工具」?该用 visit 时用 visit、参数传得准、不乱调不存在的工具,这要靠构造高质量的工具调用数据加上强化学习,是后面数据与训练章节的主题。


🏆 Part 3 · 验收串题 | ~10%

关联面试题(5 道,覆盖 5 个角度)

  1. 【工具设计与注册】 在 AI Agent 系统中,如何设计和实现可供 Agent 调用的外部工具(搜索、计算等)?
  2. 【参数校验】 工具调用(Tool Calling)过程中,如何确保模型生成的参数格式符合要求?
  3. 【实现方式对比】 Function Calling 与 Toolformer 在实现大模型工具调用上有什么区别?
  4. 【组件连接】 构建 Agent 系统时,如何设计内部各组件(工具 / 网关 / 调度等)的连接与通信?
  5. 【框架对比】 系统比较主流 Agent 框架(LangChain / LlamaIndex 等)在工具集成上的差异。

自检清单

  • 能说出一个工具的三个组成(名字 / 说明书 / 参数),以及模型「怎么知道有哪些工具」
  • 能在本机给 mini-agent 配齐 search/visit/calc,并解释「为什么改一句 description 模型行为就变」
  • 能讲清四个核心工具(Search/Visit/Scholar/Python)各自的分工,以及 Visit 为什么要做相关性抽取
  • 能讲清统一工具网关的价值(解耦 + 统一鉴权 / 限流 / 日志 / 缓存)
  • 能说出工具容错的几招(重试 / 降级 / 超时 / 重复检测 / 参数校验)和缓存与时效的矛盾
  • 能讲清 function calling vs 文本标签解析的取舍

学完这一章,你的调研 Agent 已经「手脚齐全」了,会搜、会读、会算,还知道怎么在生产里稳定运行。下一章,我们解决一个被工具问题牵出来的新麻烦:几十轮工具调用下来,信息这么多,Agent 怎么记住该记的、忘掉该忘的,长程记忆管理。