数据截至 (上游 commit f72f75d8c3eb)
02 · 工具集
本章讲 agent 手里的五样工具:各自怎么实现、派发时的分支逻辑、以及为什么
visit是工程含量最高的一支。读完你能讲清每个工具吃什么、吐什么、坑在哪。
全景:五个工具
工具在 react_agent.py:31-38 注册进 TOOL_MAP,模型按 SYSTEM_PROMPT 里的签名调用。
| 工具名 | 干什么 | 底层依赖 | 文件 |
|---|---|---|---|
search | 批量 Google 网页搜索,返回前若干结果的标题+摘要 | Serper API | tool_search.py |
google_scholar | 批量学术搜索,带引用数/发表信息 | Serper /scholar | tool_scholar.py |
visit | 抓网页 → LLM 摘要成「证据+总结」 | Jina Reader + 摘要 LLM | tool_visit.py |
PythonInterpreter | 沙箱里执行 Python | SandboxFusion | tool_python.py |
parse_file | 解析本地 PDF/DOCX/音视频等 | qwen_agent 文件解析 + VideoAgent | tool_file.py |
所有工具都继承 qwen_agent 的 BaseTool,用 @register_tool(..., allow_overwrite=True) 注册,核心方法都是 call(params) -> str。
3.1 search / scholar:最简单的一对
它要解决的小问题: 给模型一个「谷歌一下」的能力,而且一次能搜多条 query。
Search.call 接受一个 query 数组,串行逐条搜再用 \n=======\n 拼接(tool_search.py:113-130)。单条搜索走 Serper(google_search_with_serp, tool_search.py:38-105),有个贴心细节:
# tool_search.py:39-56 —— 中文 query 自动切到中国区
if contains_chinese_basic(query):
payload = {"q": query, "location": "China", "gl": "cn", "hl": "zh-cn"}
else:
payload = {"q": query, "location": "United States", "gl": "us", "hl": "en"}
返回时把每条结果格式化成 序号. [标题](链接)\n发布日期\n来源\n摘要(tool_search.py:83-102),这样模型看到的是干净的 Markdown 列表。
Scholar 几乎一样,只是打 Serper 的 /scholar 端点,并额外抽取 citedBy(引用数)、publicationInfo、pdfUrl(tool_scholar.py:60-88)。区别:Scholar 用 ThreadPoolExecutor 并发搜多条(tool_scholar.py:106-108),而 search 是串行——一个小 小的不一致。
3.2 visit:工程含量最高的一支
它要解决的小问题: 网页动辄几万字,不能整页塞回上下文。得先抓下来,再压缩成与当前目标相关的证据。
思路: 分两步——(1) 用 Jina Reader 把 URL 抓成干净文本;(2) 拿一个独立的摘要 LLM,按 EXTRACTOR_PROMPT 把长文提炼成 {rational, evidence, summary} 三段 JSON。
图示(怎么读:从左到右是 visit 内部两阶段流水线):
url ──► html_readpage_jina ──► 长文本 ──► 截到 95K token ──► 摘要LLM(EXTRACTOR_PROMPT)
(Jina, 最多重试8次) │
▼
回填给主循环 ◄── "Evidence: … / Summary: …" ◄── 解析出 evidence+summary
关键实现点:
-
抓取靠 Jina。
jina_readpage打https://r.jina.ai/{url},最多重试 3 次(tool_visit.py:132-167);外面又套html_readpage_jina最多试 8 次(tool_visit.py:169-177)。 -
先截断再摘要。 抓来的内容先
truncate_to_tokens(content, 95000)(用tiktoken的cl100k_base编码,tool_visit.py:23-32),避免撑爆摘要模型。 -
摘要用另一个 LLM,不是主模型。
call_server读API_KEY / API_BASE / SUMMARY_MODEL_NAME环境变量,调一个 OpenAI 兼容端点(tool_visit.py:99-129)。这是「主模型思考、副模型压缩」的分工。 -
摘要失败层层降级。 若摘要太短,循环里逐步把内容截更短再重试(
0.7×递减,最后砍到 25000 字,tool_visit.py:202-221);解析 JSON 失败也重试 3 次(tool_visit.py:226-232);全失败则返回一句「网页无法处理」的兜底文本(tool_visit.py:234-237)。这套多级容错是 visit 最见功力的地方,详见 03。 -
多 URL 串行 + 900 秒总闸。 传数组时逐个抓,总耗时超 900 秒后剩下的直接返回兜底文本(
tool_visit.py:83-94)。
最终回填格式固定为(tool_visit.py:239-241):
The useful information in {url} for user goal {goal} as follows:
Evidence in page: {evidence}
Summary: {summary}
3.3 PythonInterpreter:唯一走沙箱的工具
它要解决的小问题: 让模型能算数/处理数据,又不能让它在你机器上乱跑代码。
思路: 代码不在本地执行,而是丢给 SandboxFusion(字节开源的代码沙箱)远程跑。端点从环境变量 SANDBOX_FUSION_ENDPOINT 读,可配多个(逗号分隔,tool_python.py:20-25)。
call 里每次随机挑一个端点,最多重试 8 次(tool_python.py:76-109),把 stdout/stderr 拼成结果字符串返回。超时判断很朴素:执行时间 ≥ timeout-1 秒就当超时(tool_python.py:89-90)。
为什么它的调用格式特殊? 回顾 01 §3.5:主循环发现 tool_call 里含 "python" 就走单独分支,直接从 <code></code> 里抠代码传进来(react_agent.py:162-167)——绕开 JSON,因为代码塞 JSON 太容易转义炸。工具描述里明写了这个约定(prompt.py:11-25)。
3.4 parse_file:解析本地文件
它要解决的小问题: 用户上传了 PDF/表格/音视频,得先把它们变成模型能读的文本。
FileParser.call 是 async 的(tool_file.py:112-141),按扩展名分流:非 .mp3 走通用文件解析器 SingleFileParser,音视频走 VideoAgent。解析后若总 token 超 DEFAULT_MAX_INPUT_TOKENS 就调 compress 压一遍(tool_file.py:93-96)。因为是 async,主循环里得用 asyncio.run(...) 包一层调用(react_agent.py:233-237)。
3.5 工具派发的坑:custom_call_tool
派发逻辑在 react_agent.py:228-247。有几处值得注意:
-
参数被套了一层。
tool_args["params"] = tool_args(react_agent.py:230)——把参数字典塞进自己的params键,以适配 qwen_agent BaseTool 的取参约定。 -
parse_file 特判文件根目录。 只有它显式传
file_root_path="./eval_data/file_corpus"(react_agent.py:236),对应 README 里「把文件放这个目录」的约定。 -
找不到工具返回错误字符串,不抛异常(
react_agent.py:246-247),交由主循环当作普通观察回填——模型有机会看到「工具不存在」并改正。
代码地图
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 网页搜索 | inference/tool_search.py | Search.google_search_with_serp |
| 学术搜索 | inference/tool_scholar.py | Scholar.google_scholar_with_serp |
| 抓网页 | inference/tool_visit.py | Visit.jina_readpage、html_readpage_jina |
| 网页摘要成证据 | inference/tool_visit.py | Visit.readpage_jina、Visit.call_server |
| 摘要提示词 | inference/prompt.py | EXTRACTOR_PROMPT |
| Python 沙箱 | inference/tool_python.py | PythonInterpreter.call |
| 文件解析 | inference/tool_file.py | FileParser.call、file_parser |
| 工具派发 | inference/react_agent.py | MultiTurnReactAgent.custom_call_tool |