拆穿 tools=tools:从工具调用到 Deep Research 架构
SDK 里那行 tools=tools 到底做了什么?从 Prompt 注入与拼接重传两层真相出发,论证多 Agent 与工具调用在结构上的同构性,最后拆解一个真实的 Orchestrator-Workers 项目。
TL;DR
Agent = LLM + Planning + Memory + Tools 这个公式大家很熟,但实际写代码时我们只是机械地传入一个 tools 参数。 这篇先拆穿它:第一层是 Prompt Engineering,SDK 偷偷把 JSON Schema 转成 System Prompt; 第二层是「暂停 → 执行 → 拼接 → 重传」,模型压根不会自己拿到 Python 的执行结果。 在此之上再进一步:把工具函数换成子 Agent,会发现单 Agent 的四种工具调用形态与多 Agent 的四种架构模式完全对应。 最后用一个真实的 Deep Research 项目验证——Orchestrator 规划派发,Worker 在预算约束下跑 OODA 回路, 再靠质量检查与差距分析形成迭代闭环。而 Agent 之间从不「聊天」,只互传 JSON。
目录
Agent 的经典公式大家很熟悉:
Agent = LLM + Planning + Memory + Tools
但在实际写代码时,我们往往只是在 SDK 里机械地传入一个 tools 参数,却没真正理解两个关键问题:
- 工具定义是如何「进入模型脑子」的呢?
- 模型输出
tool_call之后,又是如何拿到 Python 执行结果并完成最终回答?
这篇文章是我把工具调用的底层机制梳理清楚之后,并在这个基础上再进一步:如果把「工具函数」换成「子 Agent」,会发生什么呢?
读完之后就会发现,多 Agent 协作与工具调用在结构上高度同构:只是调用对象从「无状态函数」变成了「有状态策略单元」。
一、从 LLM 到 Agent:工具调用的真相
我们在使用 OpenAI 或 Qwen 的 SDK 时,代码通常是这样写的:
def get_response(messages): completion = client.chat.completions.create( model="qwen-plus", messages=messages, # 关键就是这行代码 tools=tools, ) return completion看起来非常简单,只要把定义好的工具列表 JSON Schema 传进去,再加上一句 tools=tools,模型自然就知道该怎么调用工具了。可是这底层到底发生了什么?
真相可以分为以下两层。
第一层:Prompt Engineering
如果不传 tools=tools,能让模型用工具吗?完全可以。
当传入 tools 参数时,SDK 或大模型后端其实在偷偷做一件事:把这些工具的定义(JSON Schema),转换成了一段 System Prompt,塞进了对话的开头。
SDK 内部其实就是帮我们做了一次「文本格式化」。可以尝试手动实现这个过程。
第一步:欺骗模型(Prompt 注入)
直接在 System Prompt 里告诉模型:「你是一个有工具的助手。工具有这些:[这里塞入 JSON 定义]。如果要用,请按这个格式输出:<tool_call>...</tool_call>。」
# 1. 把工具定义转换成字符串,塞进 Promptsystem_prompt = f"""你是一个助手。你可以使用以下工具:<tools>{tools_content}</tools>如果要调用工具,请务必输出 XML 格式:<tool_call>{{"name": "函数名称", "args": {{"参数名": "参数值"}}}}</tool_call>"""
# 2. 整理 messages 内容messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": "北京天气怎么样"}]
# 3. 发送给 LLM(注意:这里不传 tools 参数,完全靠 Prompt 指引)completion = client.chat.completions.create( model="qwen-plus", messages=messages,)第二步:手动解析(正则提取)
当模型返回文本时,它并不会真的运行代码,只是输出了上面 prompt 中要求的一段 XML 文本:
<tool_call>{"name": "get_weather", "args": {"loc": "Beijing"}}</tool_call>这时候,我们需要用正则表达式把运行工具函数需要的信息抠出来:
if "<tool_call>" in response.content: match = re.search(r"<tool_call>(.*?)</tool_call>", response.content) tool_call_data = json.loads(match.group(1)) # 拿到 name 和 args,手动执行 Python 函数 execute_function(tool_call_data["name"], tool_call_data["args"])所谓的工具调用,第一层本质就是 Prompt Engineering(提示词工程) + Structured Output(结构化输出提取)。
第二层:拼接与重传
光有 Prompt 还不够。最容易疑惑的地方在于:模型吐出 tool_call 之后,它是怎么拿到 Python 运行的结果并回答用户的呢?
(之前的文章深度解析 Memory 本质:Agent 和 Chatbot 的差异中有详细写过。这个流程的核心就是:暂停 → 执行 → 拼接 → 重传。)
这里再以「查询巴黎天气」为例简单解释一下。
第一回合:LLM 的开条子时刻
- 用户提问:「巴黎天气怎么样?」
- LLM 思考:它收到 System Prompt(你有工具)和 User Message。它发现自己不知道天气,但有个
get_weather工具。 - LLM 暂停并输出:LLM 停止生成自然语言,而是吐出一个 JSON(或者上面定义的 XML):
{"name": "get_weather", "args": {"city": "Paris"}}到此为止,第一次请求结束了。模型处于挂机状态。
中场休息:Python 的跑腿时刻
这时候完全没有 LLM 的事了,全是 Python 代码在干活:代码捕获到检查单(tool_calls),去调用天气 API,拿到了 "14.2°C"。
第二回合:带着结果,重新排队
为了让 LLM 能回答用户,必须把刚才发生的一切,打包成新的历史记录,重新发给 LLM。这一步叫「拼接」,需要构造一个包含三条信息的新列表:
User:「巴黎天气怎么样?」Assistant:(第一轮模型生成的 tool_calls)「我要调用 get_weather……」Tool:(Python 跑出来的结果)「14.2°C」
然后重传:再次调用 client.chat.completions.create。
LLM 看到这三句话,恍然大悟:「哦,我刚才要查巴黎天气,现在温度(14.2 度)来了。」于是它生成最终回复:「巴黎现在 14.2 度,比较凉爽。」
理解了这两层真相(Prompt 定义 + 拼接重传),就彻底搞懂了单 Agent 的工具调用。接下来就可以进阶了:如果把工具换成另一个 Agent,会发生什么呢?
二、从单 Agent 到多 Agent:架构的同构性
如果任务太复杂,一个 Agent 搞不定,这时就会想到用多 Agent 协作完成。
多 Agent 听起来很深奥,但其实,多 Agent 本质上可以看成把「工具调用」提升了一个抽象层级:
- 单 Agent:LLM 调用一个个具体的 Python 函数(Tools)
- 多 Agent:总控 Agent 调用一个个具备推理、记忆和工具能力的子系统(Sub-Agents)
从这两张图就会发现两者的同构性。并且单 Agent 的四种工具调用形态,与多 Agent 的架构模式可以完美对应。
1、单一工具调用 ≈ 单一子 Agent
这是最简单的形态。
- 工具层:用户问天气 → 模型生成 Tool Call → 执行 weather 函数
- Agent 层:简单的 SearchAgent。用户提问 → Agent 规划 → 搜索并回答
线性执行,一次往返。
2、多工具选择(Router)≈ 路由式多 Agent
- 工具层:给模型一堆工具(计算器、翻译、搜索),模型在单轮对话中决定用哪一个
- Agent 层:给总控 Agent 一堆子 Agent(数学专家、翻译专家、搜索专家),总控根据问题分发任务
这是分类问题。不需要复杂的编排,只需要准确的「意图识别」。
3、多轮链式工具(Chain)≈ 链式 / 接力 Agent
这是最常见的进阶形态。
工具层(While 循环):LLM 先调 web_search → System 回填搜索结果 → LLM 读结果,发现全是英文,决定调 translate_text → System 回填翻译结果 → LLM 最终总结。
Agent 层:策划 Agent → 撰写 Agent → 审核 Agent。后一个 Agent 的输入严格依赖前一个 Agent 的输出。
这是状态机。核心在于 while True 循环,直到满足停止条件(如 tool_calls 为空)。
4、单轮并行工具 ≈ 并行多 Agent
工具层:LLM 一次性输出多个 Tool Call:[get_weather(Beijing), get_weather(Paris), get_stock(AAPL)],代码层用 ThreadPoolExecutor 并发执行,最后汇总。
Agent 层:这也是 Deep Research 的核心雏形。面对一个大问题,拆解成 n 个子问题,由 n 个 Worker Agent 同时并行处理,最后由 Leader Agent 汇总。
目的是提升吞吐,利用 LLM 的并发规划能力。
总结一下:
- 工具是无状态的函数,输入即输出。
- 子 Agent 是有状态的「大工具」,它内部包含了「目标、上下文、反思循环」。
多 Agent 结构就像工具调用的升级版:调度器调用的不再是死函数,而是一个个能独立完成子任务的策略单元。
(无论外面的架构怎么变——路由、并行、串行——每一个干活的 Agent 内部,基本都遵循一个经典的闭环:Plan-Act-Reflect。之前的文章「规划-行动-反思」架构:Agent 是如何进行反思的?有写到相关细节。)
三、拆解 Deep Research 架构
现在,就可以用上面的知识,来拆解一个真实的多 Agent 项目了。
这个项目是标准的 Orchestrator-Workers 模式,能够做到:用户给一个相对模糊的课题,它能自动拆解子任务、并行检索、归纳分析、发现缺口并自动补全,最终生成一份结构化的研究报告。
它的核心代码结构如下:
orchestrator.py:大脑。负责规划、派发、汇总、验收与迭代。workers.py:手脚。负责执行具体的检索、整理与单点研究(包含 OODA 循环)。tools.py:工具箱。封装搜索 API 与 LLM 调用,并提供 JSON 提取等基础能力。
下面跟踪一条指令:「分析 2026 年人工智能的发展趋势」,看看它是如何在代码中流转的。
1、大脑规划:从模糊指令到 JSON 规划
一切始于 orchestrator.py。主调度 Agent 拿到的第一个动作不是去搜,而是先想清楚怎么搜。
在代码中,这对应一个 decompose_task 函数流程:它把模糊任务交给 LLM,让模型输出一个可执行的 TaskPlan,其中最关键的是 subtasks 列表与 query_type / recommended_worker_count 等字段:
{ "query_type": "breadth_first", "subtasks": [ {"id": 1, "description": "2026 AI 技术架构趋势(端侧模型、推理加速)"}, {"id": 2, "description": "2026 AI 产业落地(医疗、金融、制造)"}, {"id": 3, "description": "全球 AI 监管与合规政策"} ], "recommended_worker_count": 3}2、手脚的并发:朴素的 ThreadPoolExecutor
拿到 subtasks 列表后,怎么让多个 Worker 同时干活呢?
代码中没有引入复杂的 Asyncio 或多进程,而是选择了对于网络检索型 IO 任务非常实用的 ThreadPoolExecutor。在 dispatch_workers 函数中,Orchestrator 会为每个 subtask 创建一个独立 Worker 实例,把它们扔进线程池并行跑:
def dispatch_workers(self, subtasks): actual_workers = min(len(subtasks), self.max_workers) with ThreadPoolExecutor(max_workers=actual_workers) as executor: futures = [] for subtask in subtasks: worker = self.worker_factory(subtask) # 每个 subtask 一个 worker 实例 futures.append(executor.submit(worker.execute, subtask)) results = [f.result() for f in as_completed(futures)] return results这就是「单轮并行」的真相:把 n 个子任务扔进线程池,让它们同时去调用搜索 API 与 LLM。每个 Worker 都是独立实例,互不共享内部状态。
3、Worker 的核心:带预算的 OODA 循环
现在视角切换到 workers.py。每个被扔进线程池的 Worker 内部是在做什么呢?
代码中负责实现的 SearchWorker 采用了一个 OODA + 预算的研究回路:
- 预算限制(max_queries / max_cycles)由任务复杂度(simple / medium / complex)自动估算并配置
- OODA 每轮会「观察(搜索)→ 定向(粗评估结果)→ 决策(是否补搜、补什么)→ 行动(生成下一轮 query)」
def execute(self, subtask): budget = adjust_budget(assess_complexity(subtask)) # max_queries + max_cycles queries = subtask.search_queries if not queries: queries = expand_queries_with_llm(subtask) # 可能会调用一次 LLM
all_results = [] for cycle in range(budget.max_cycles): if budget.remaining_queries <= 0: break
# Observe:执行若干 query 的搜索 new_results = [] for q in pick_queries(queries, budget.remaining_queries): new_results += search_client.search(q) budget.consume_query() all_results += new_results
# Orient / Decide:来源粗评估 + 信息缺口识别(偏启发式) gaps = identify_information_gaps(subtask, all_results) if not gaps: break
# Act:基于 gaps 生成下一轮 queries queries = generate_followup_queries(gaps)
# 最后:调用 LLM 把 all_results 格式化 + 归纳为结构化输出 context = format_search_context(all_results) return llm_summarize_to_structured_result(subtask, context)主程序只给目标,Worker 在预算约束下通过多轮搜索补齐信息,再把证据上下文整理成结构化结果交回 Orchestrator 进行汇总与撰写。
4、闭环的灵魂:迭代与查漏补缺
Deep Research 之所以是 Deep,在于它不只有拆解 → 并行 → 汇总,还引入了质量检查 + 差距分析 + 补充研究 + 修订的迭代闭环。
在 orchestrator.run() 中,主流程大致是:
def run(self, task): # 1) 初次研究 task_plan = self.decompose_task(task) worker_results = self.dispatch_workers(task_plan.subtasks) report = self.synthesize_results(worker_results)
# 2) 迭代优化(注意:有 min_iterations + quality_threshold 双重条件) for i in range(self.max_iterations): quality = self.check_quality(report) if i + 1 >= self.min_iterations and quality.score >= self.quality_threshold: break
# (可选)收益递减检测:模型判断是否「继续已不划算」 if self.enable_diminishing_returns_check: if self.check_diminishing_returns(...).should_stop: break
# Gap Analysis:模型指出缺口 + 生成 supplementary_tasks gap_info = self.analyze_gaps(report, task, quality) supplementary_tasks = gap_info["supplementary_tasks"]
# 再并行补漏 new_results = self.dispatch_supplementary_workers(supplementary_tasks)
# 修订报告 report = self.refine_report(report, gap_info, new_results)
return report报告不是一次写成,而是写完初稿后自己评审,发现缺口(比如监管条款没说清、关键数据缺引用、对比维度不够),然后动态生成补充子任务,让 Worker 去补证据,补回来后再修订报告。
在拆解完整个 orchestrator.py 和 workers.py 后,是否发现了一个有趣的细节?
Agent 之间从不「聊天」,而是互传 JSON。
Orchestrator 下发的是结构化的 SubTask,Worker 回传的是结构化的 Dict。多 Agent 协作的本质,不是让模型互相聊天,而是定义一套清晰的 API 接口标准。
最后
从 tools=tools 到 Deep Research,本质是同一条链路的放大:Prompt 约束结构化输出 + 暂停 → 执行 → 拼接 → 重传。
单 Agent 在循环里按开关,多 Agent 在总控调度下并行按开关、再用评估与补漏迭代优化。
理解了这些底层机制与调度原理,就可以自己摸索着设计出真正解决复杂问题的系统了。
核心结论
标注「判断」「假设」的是我的看法而非事实;标注「已推翻」的保留在这里,不删除。
不传 tools 参数也完全可以让模型用工具。SDK 内部做的只是一次文本格式化——把工具的 JSON Schema 转换成一段 System Prompt 塞进对话开头。工具调用的第一层本质是 Prompt Engineering 加结构化输出提取。#
把握较大
模型吐出 tool_call 之后并不会自己拿到执行结果。必须由代码捕获调用、在外部执行、把 User / Assistant / Tool 三条消息拼成新历史,再重新发一次请求。这是第二层真相。#
把握较大
多 Agent 本质上是把工具调用提升了一个抽象层级。单 Agent 的四种形态——单一调用、Router 路由、Chain 链式、并行——与多 Agent 的四种架构模式完全同构,只是调用对象从无状态函数变成了有状态的策略单元。#
判断 · 把握较大
Deep Research 里的 Worker 不是无限搜索,而是跑一个带预算的 OODA 回路:由任务复杂度自动估算 max_queries 与 max_cycles,在预算内观察、评估缺口、生成下一轮 query。没有预算的循环在生产里就是烧钱。#
判断 · 把握较大
多 Agent 协作的本质不是让模型互相聊天,而是定义一套清晰的 API 接口标准。Orchestrator 下发结构化 SubTask,Worker 回传结构化 Dict,全程没有一句自然语言对话。#
判断 · 把握较大
引用本文
晨旭,《拆穿 tools=tools:从工具调用到 Deep Research 架构》,晨光里的AI,2026-01-03
[晨旭:《拆穿 tools=tools:从工具调用到 Deep Research 架构》](https://chenxu.xin/writing/tools-to-deep-research)带进你的 AI 继续追问
这篇文章有一份干净的 Markdown 原文,可以直接交给任何模型读,不用复制粘贴。