源文件:chapter4/active-tool-discovery/README.md 实验 4-6:主动工具发现(Active Tool Discovery) 《深入理解 AI Agent》配套代码 · ★★★ 在 126 个跨领域工具的工具库上,一次运行对比三种"工具发现"策略——全量注入、 检索预筛选(一次性语义预选 top-n)、主动发现(执行中按需 )—— 并输出统一的对比表(准确率 / 注入 token / 延迟)。量化全量注入的 token 浪费,展示主动发现 如何用嵌入向量相似度把上百个工具收敛成几条精准候选,同时揭示"一次性预筛选"在多步跨领域 任务上的内在局限。无 API key 也能跑: 用本地嵌入 + mock 模型自检机制。
源文件:chapter4/active-tool-discovery/README.md
《深入理解 AI Agent》配套代码 · ★★★
在 126 个跨领域工具的工具库上,一次运行对比三种"工具发现"策略——全量注入、
检索预筛选(一次性语义预选 top-n)、主动发现(执行中按需discover_tools)——
并输出统一的对比表(准确率 / 注入 token / 延迟)。量化全量注入的 token 浪费,展示主动发现
如何用嵌入向量相似度把上百个工具收敛成几条精准候选,同时揭示"一次性预筛选"在多步跨领域
任务上的内在局限。无 API key 也能跑:python demo.py --offline用本地嵌入 + mock 模型自检机制。
当一个 Agent 拥有上百个工具时,常见做法是把全部工具的 JSON schema 一次性塞进 system prompt。
这会带来两个问题:
web_search /google_search / universal_search)和专用工具一起调用,甚至用通用搜索替代专用工具主动发现只在 system 里保留少量基础工具 + 一个 discover_tools(need) 元工具。模型遇到能力缺口时,
用自然语言描述需求,系统用嵌入相似度从工具库检索 3-5 个最相关的专用工具,把它们的 schema 作为
user message 追加进对话(保护 system 前缀的 KV Cache),并更新状态栏可用工具列表。
tools_library.py 126 个跨领域工具(finance/web/arxiv/github/geo/weather/media/... 共 17 个领域) 每个工具有真实 name/description/parameters;执行为轻量 mock(重点是"选对工具") 其中故意混入 8 个"通用/近义"工具(web_search 等),它们的描述夸大自己无所不能 select_tools(size):按 --tool-set-size 截取子集,演示"工具集越大全量注入越吃亏" discovery.py 可插拔嵌入后端 + 工具向量索引;OpenAIEmbedder 用 text-embedding-3-small 生成向量 并缓存到 .cache/;search(need) = 把 need 向量化后与工具向量做余弦相似度返回 top-k agent.py 三种策略的 ReAct 循环(文本协议:模型每步输出一个 JSON 工具调用) - run_full_injection:126 个工具 schema 全部写进 system prompt - run_retrieval_prefilter:按初始查询一次性检索 top-n 工具注入(书中"检索式预筛选") - run_active_discovery:基础工具 + discover_tools,执行中按需检索加载 offline_backend.py 离线后端:LocalEmbedder(本地哈希词袋嵌入)+ MockChatClient(脚本化 mock 模型), 让 --offline 无需任何 API key 即可跑通全流程(token/延迟真实,准确率仅反映启发式路由) demo.py 对同一组任务分别跑所选策略,打印 token / 延迟 / 调用轨迹 / 是否精确选对,并汇总对比表
为什么用"文本注入 + 文本解析"而不是 OpenAI 原生 function calling?
原生 function-calling 接口对工具选择做了很强的约束优化,即使上百个工具也极少选错,无法体现书中
所述的"超长上下文指令遵循退化"。把 schema 当作纯文本塞进 prompt、让模型自己以 JSON 输出工具调用,
才是控制组的真实机制,也才能观察到退化。这也正是书中"把 schema 注入 system prompt(几万 token)"的写法。
为什么嵌入检索能避免错选? 通用工具 web_search 的描述"什么都能做",语义被稀释;而专用工具
(如 search_news)描述聚焦。对一个聚焦的 need("获取特斯拉最近的新闻"),聚焦的专用工具余弦相似度
更高、排在前面,通用工具往往进不了 top-k,于是根本不会被加载 —— 检索层天然起到了"精度过滤"作用。
检索预筛选为什么不够? 检索预筛选(run_retrieval_prefilter)只按初始查询做一次语义匹配、
一次性注入 top-n 工具。对"查股价 + 搜新闻"这类多步跨领域任务,初始查询的向量往往偏向第一个领域,
第二个子任务需要的专用工具可能挤不进 top-n,模型执行到一半才发现"想调用的工具根本没在清单里"——
这正是书中指出的一次性匹配的内在局限。主动发现把"发现"延后到执行中、按每个真实浮现的 need 分别检索,
从而补齐这一缺口(离线自检里可直接观察到:检索预筛选在半数多步任务上漏掉了第二个工具,见下表)。
pip install -r requirements.txt # 方式 A:离线机制自检(无需任何 key;token/延迟真实,准确率仅反映启发式路由) python demo.py --offline # 方式 B:真实模型(体现小模型"指令遵循退化"需要真实 LLM) cp env.example .env # 填入 OPENAI_API_KEY(chat 与 embeddings 都用 OpenAI) # 兜底:若无 OPENAI_API_KEY 但设置了 OPENROUTER_API_KEY,chat 会自动改走 OpenRouter #(模型映射到 openai/gpt-5.6-luna 等),工具检索退用本地哈希嵌入(OpenRouter 无 embeddings 接口)。 python demo.py # 全部 8 任务 × 三种策略 python demo.py --strategies full,discovery # 只跑其中两种策略对比 python demo.py --tasks finance+news,crypto+news # 只跑指定任务(逗号分隔) python demo.py --tasks 'opinion(诱导)' # 含括号的任务 id 记得加引号 python demo.py --tool-set-size 20 # 缩小工具集,看全量注入的劣势如何随规模放大 python demo.py --query '查英伟达股价再搜点相关新闻' --offline # 临时单条自然语言任务 python demo.py --offline --output results/offline.json # 导出结构化结果
默认模型 gpt-5.6-luna,可用 --model 或 env 覆盖:python demo.py --model gpt-5.6-luna。
首次运行会为工具生成嵌入向量并缓存到 .cache/,之后复用。python demo.py --help 查看全部参数
(--query / --tasks / --strategies / --tool-set-size / --top-k / --prefilter-n / --model / --embed-model / --max-steps / --offline / --output)。
MODEL=gpt-4.1-mini python demo.py(chat 模型);EMBED_MODEL=text-embedding-3-large 换嵌入模型.cache/ 索引)。OpenAI() 会自动读取环境变量 OPENAI_BASE_URL,OPENAI_BASE_URL=https://your-gateway/v1(该端点需同时提供 chat 与 embeddings)。tools_library.py 里的 TASKS(每条含 prompt 与判分用的能力槽位),或用--tasks 只跑其中几条,或用 --query 传一句临时需求;想扩充工具库同样在 tools_library.py 的 ALL_TOOLS 中增删。--offline 用 offline_backend.py 的本地哈希嵌入 + 脚本化 mock 模型,无需任何 key,python demo.py --offline)下表为一次真实的 --offline 运行(8 任务 × 三策略)。token/延迟是 tiktoken/wall-clock 真实测量;
准确率仅反映脚本化启发式路由,不代表真实模型能力——mock 模型是"强路由器",不会退化,所以全量注入也拿满分。
| 策略 | 精确选对 | 任务完成 | 平均注入 token | 总注入 token | 平均延迟(s) |
|---|---|---|---|---|---|
| 全量注入 | 8/8 | 8/8 | 11630 | 93040 | 0.008 |
| 检索预筛选 | 4/8 | 4/8 | 1030 | 8236 | 0.006 |
| 主动发现 | 8/8 | 8/8 | 974 | 7796 | 0.010 |
离线自检要传达的两个真实、可复现的结构性结论:
--tool-set-size 20 缩小工具集,差距收敛到 ~1.8×——印证"工具越多、academic(诱导) 的 top-10 里根本没有 arxiv_search),模型执行到一半调不到工具 →need 分别检索,8/8 补齐。说明:下表是一次真实 LLM 运行(
python demo.py --model gpt-5.6-luna,8 任务 × 三策略,OpenAI 直连
chat +text-embedding-3-small检索)。gpt-5.6-luna 是推理型模型,仅支持默认temperature=1
(不支持temperature=0,代码遇到该报错会自动回退到默认温度),故本次为单次、非确定性运行;
token/延迟为真实测量,逐任务的选择结果可能随采样波动。判定:✅=精确选对(覆盖全部能力槽位且未错选
通用兜底工具);⚠️=完成但顺手错选了通用工具;❌=出错(漏用专用工具或中途放弃、0 次工具调用)。
| 任务 | 全量注入 | 检索预筛选 | 主动发现 | 全量 token | 发现 token |
|---|---|---|---|---|---|
| finance+news | ✅ | ❌ | ✅ | 11630 | 883 |
| arxiv+download | ✅ | ❌ | ✅ | 11630 | 927 |
| github+viz | ❌ | ❌ | ❌ | 11630 | 295 |
| weather+calendar | ❌ | ✅ | ✅ | 11630 | 1055 |
| forex+weather | ✅ | ✅ | ❌ | 11630 | 295 |
| crypto+news | ❌ | ⚠️ | ❌ | 11630 | 295 |
| opinion(诱导) | ⚠️ | ❌ | ✅ | 11630 | 688 |
| academic(诱导) | ⚠️ | ⚠️ | ❌ | 11630 | 295 |
| 精确选对 | 3/8 | 2/8 | 4/8 | ||
| 任务完成 | 5/8 | 4/8 | 4/8 | ||
| 总注入 token | 93040 | 4733 |
(检索预筛选平均 971 token/任务、总 7768;三策略平均延迟约 11.5 / 9.6 / 10.7 s,均为本次真实测量。)
token 节省依旧稳健(且更悬殊):全量注入每任务固定注入 11,630 token;主动发现按需加载后仅
295~1,055 token,合计 93,040 → 4,733(~19.7×)。需诚实说明:本次比值偏大,部分是因为
gpt-5.6-luna 在若干任务上直接放弃、根本没触发 discover_tools(此时只注入 3 个基础工具 = 295 token)。
即便如此,"全量注入固定重复计费上万 token、按需发现只注入千级 token"这一结构性收益不受影响。
书中核心现象在两个"诱导任务"上如实复现:措辞偏泛时,全量注入会顺手抓通用兜底工具——
opinion(诱导)("特斯拉最近的新闻舆论风向"):全量注入调用了 search_news, search_news, web_search, search_tweets,把通用的 web_search 也用上(⚠️ 错选);主动发现检索到search_news / get_news_by_source / ...(没有 web_search),只调用专用新闻工具,干净选对(✅)。academic(诱导)("量子计算最新科研进展"):全量注入一口气调用了 8 个工具,其中google_search / universal_search / ask_knowledge_base 三个都是通用兜底(⚠️);检索预筛选也错选了google_search / universal_search。这正是书中"上百工具的工具墙 + 措辞含糊 → 广撒网抓通用工具"的写照。本次运行暴露的另一类真实行为(与早期 gpt-4o-mini 运行不同,须如实记录):gpt-5.6-luna 是偏保守的
推理型模型,在多个任务上没有调用(mock)工具就提前 finish,理由多为"无法访问实时数据/工具"
(如 github+viz、weather+calendar 的全量注入,以及 forex+weather、crypto+news、academic 的
主动发现,均出现 0 次工具调用)。这压低了三种策略的绝对准确率,也意味着本次得不出"清晰任务下模型
面对工具墙一律选对"的结论——恰恰相反,放弃/漏步成了主要失分点,且这类失分在全量注入与主动发现上都存在。
如实说明的边界:
temperature=1,逐任务结果具随机性;重复运行时哪些任务"放弃"、哪些--offline 表。一句话:在 gpt-5.6-luna 上,主动工具发现最稳的收益仍是 token(本次 ~19.7×);在措辞含糊、通用工具
易被误用的"诱导任务"上,嵌入检索确实把web_search / google_search / universal_search等夸大其词的
通用工具挡在候选之外。但这一版真实运行也提醒:强推理模型保守的"放弃"行为会同时拉低各策略的绝对准确率,
单次结果需按上表如实解读。
这一节回答一个直接的问题:模型变强,这套"主动工具发现"脚手架是不是就没用了?
我们把上面的 gpt-5.6-luna(强)结果,与同样 8 任务 × 三策略、OpenAI 直连 chat +text-embedding-3-small检索的 gpt-4o-mini(弱) 真实运行放在一起对照
(python demo.py --model gpt-4o-mini,2026-07,判定口径同上)。结论是:脚手架有两种价值,
一种随模型变强而淡出,另一种与模型强弱无关、始终存在。
弱模型 gpt-4o-mini 真实汇总:
| 策略 | 精确选对 | 任务完成 | 总注入 token | 平均延迟(s) |
|---|---|---|---|---|
| 全量注入 | 5/8 | 8/8 | 93040 | 8.38 |
| 检索预筛选 | 7/8 | 7/8 | 7768 | 4.90 |
| 主动发现 | 8/8 | 8/8 | 7266 | 7.65 |
(token 93040 → 7266,~12.8× 精简。)
crypto+news 用了 web_search,两个诱导任务opinion / academic 各自把 web_search / google_search / universal_search 一并调用——opinion / academic),比 gpt-4o-mini 的 3 个更少;主动发现把这两处也擦干净,但精确率只从finish(理由多为"无法访问实时数据"),全量注入无论模型强弱都固定为 11,630 token/任务(把 126 个工具 schema 全塞进 system),
这是纯结构性开销。按需发现只注入几百到一千余 token:
discover_tools,两个模型上 token 节省都稳稳成立,且随工具集变大而放大——这份收益不因模型变强而消失,
是脚手架在"强模型时代"仍然值得保留的硬理由。
模型越强,脚手架"帮它别选错工具"的价值越淡(gpt-4o-mini 全量 5/8→发现 8/8 精确、零完成损失;
gpt-5.6-luna 仅 3/8→4/8 且完成还降了,因为它的失分是"放弃"而非"错选");但"省 token"的价值
与模型强弱无关、始终存在(弱模型 ~12.8×、强模型 ~19.7×,全量注入恒为 11,630 token/任务)。
所以在强模型上,主动工具发现的主要理由从"纠正指令遵循退化"转向"控制上下文成本"。
tools_library.py — 126 个工具定义 + select_tools 子集截取 + mock 执行 + 8 个评测任务与判分标准discovery.py — 可插拔嵌入后端(OpenAIEmbedder)+ 工具向量索引与相似度检索(discover_tools/预筛选的后端)agent.py — 三种策略(全量注入 / 检索预筛选 / 主动发现)的 ReAct 循环与 token 统计offline_backend.py — 离线后端:LocalEmbedder + MockChatClient,支撑 --offline 无 key 自检demo.py — 一键多策略对比演示(含 CLI:--query/--tasks/--strategies/--tool-set-size/--offline/--output 等)requirements.txt / env.example