Tool Call 不是工具执行:Agent 怎样安全地获得外部观察
Tool Call 不是工具执行:Agent 怎样安全地获得外部观察
前三篇依次建立了四层边界:一次 LLM Call 只是基于 Context 的推理;应用用 Request 和 Response 管理这次推理;用户目标则要先被写成 Task Contract、Plan 与可执行 Task。
C03 结束时,技术调研任务已经处于 ready:研究对象、比较维度、来源要求与验收条件都已确认,T1~T5 也有了完成条件。可它仍然没有读过任何资料。现在真正的问题出现了:模型怎样提出“去找资料”的建议?谁实际访问网页或写入证据?结果又怎样成为下一次判断的依据?
这就是 Tool Calling 要解决的连接点。
Tool Call 是模型提出的结构化执行请求;Runtime 决定是否执行、执行什么代码,并把实际结果作为 Tool Result 返回。
口语里常说“模型调用工具”,但这句话省略了最重要的责任。模型没有浏览器句柄、数据库连接或文件权限。它能生成工具名和参数;拥有权限、处理副作用、记录结果的是 Runtime。
从 C03 的 ready,到一次可观察的行动
C04 继续使用已确认的 ResearchTaskSpec。Runtime 将当前 Task 的必要信息和允许使用的工具定义放入 Request。模型先返回一项 Tool Call:
{
"type": "tool_call",
"id": "call_01",
"name": "search_sources",
"arguments": {
"query": "OpenAI Agents SDK state management official documentation",
"source_type": "official",
"limit": 3
}
}
到这里,搜索尚未发生。这与前两篇的结论完全一致:它只是一次 Response 中的结构化内容。它表明模型建议下一步做什么,并不证明网络被访问过、资料已经找到,更不证明 T1 已完成。
sequenceDiagram
participant R as Runtime
participant M as 模型服务
participant T as Tool
R->>M: Task Context + Tool definitions
M-->>R: Tool Call(name, arguments, call_id)
R->>R: 路由、校验、授权与限额检查
R->>T: 执行真实函数
T-->>R: 原始结果或异常
R->>R: 规范化并校验结果
R->>M: Tool Result(call_id, status, data/error)
M-->>R: 基于新 Observation 的下一次 Response
call_id 是这条链路的关联键。一次 Response 可以含多个 Tool Call;Runtime 必须让每份 Tool Result 回到正确的调用,而不能只附上一段无来源的日志文本。
Tool Contract:让模型能提议,让 Runtime 能拒绝
工具不是把一个函数名暴露给模型就够了。它需要一份同时服务模型和执行层的契约:
| 部分 | 回答的问题 | 主要消费者 |
|---|---|---|
name |
这是哪个稳定、可路由的能力? | 模型、Runtime |
description |
什么时候应使用,什么时候不应使用? | 模型 |
input_schema |
参数必须长成什么样? | 模型、Runtime |
output_schema |
成功结果可如何被消费? | Runtime、下游代码 |
| Runtime metadata | 是否只读、超时和审批要求是什么? | Runtime |
C04 只开放三个小工具,而不使用含糊的 do_research:
| 工具 | 责任 | 不能做什么 |
|---|---|---|
search_sources |
按查询词发现候选资料,返回摘要与 URL | 不读取正文、不保存证据 |
fetch_document |
读取一个已知 URL 的规范化正文 | 不搜索、不写入 |
save_evidence |
将已核对的主张写入当前 Task 的证据库 | 不判定真伪、不写报告 |
这种拆分不是为了把工具越拆越细,而是为了让每次跨越的边界清楚。已有 URL 且需要原文时,模型应选 fetch_document;只知道主题、需要发现候选资料时才选 search_sources。写入证据又是另一类有副作用的行动,不能被“搜索资料”悄悄带过。
以 search_sources 为例,描述应告诉模型选择条件和反例:
name: search_sources
description: >
按查询词发现候选技术资料,返回标题、URL、来源类型和摘要。
仅在需要发现资料时使用;已有 URL 或需要完整正文时不要使用。
input_schema:
type: object
properties:
query: { type: string, minLength: 3 }
source_type: { enum: [official, any] }
limit: { type: integer, minimum: 1, maximum: 10 }
required: [query, source_type, limit]
additionalProperties: false
Description 帮模型做选择,却不是安全控制。limit: 50 可能在自然语言上合理,但仍应由 Runtime 拒绝;一个格式正确的 URL 也仍可能指向不允许访问的位置。Schema 检查结构,授权与安全策略检查“这次行动是否允许”。
模型建议,Runtime 负责执行边界
一个最小执行器不需要 Agent 框架。它的职责反而应当单调、可审计:
def execute_tool_call(call, runtime_context):
contract = TOOL_CONTRACTS.get(call.name)
function = TOOLS.get(call.name)
if contract is None or function is None:
return fail("unknown_tool", "TOOL_NOT_ALLOWED", retryable=False)
errors = validate(call.arguments, contract.input_schema)
if errors:
return fail("invalid_arguments", "SCHEMA_VALIDATION_FAILED",
retryable=False, details=errors)
authorize(runtime_context, call.name, call.arguments)
raw_result = function(**call.arguments)
validate_or_raise(raw_result, contract.output_schema)
return {"ok": True, "data": raw_result}
关键不在 function(**arguments),而在它前后的防线:只从 Registry 查找工具,不能根据模型给的字符串动态导入;参数先校验;执行前检查当前 Task、用户与权限;来自外部世界的返回值也要校验,再决定是否进入 Context。
尤其是 save_evidence:Runtime 应确认 task_id 就是当前 Task,自己生成写入路径,并用幂等键处理重试。模型提出“把资料写到另一个任务”不是授权,Tool Call 从来不是通行证。
失败、空结果与成功,是三种不同的观察
工具执行层不能把所有异常压成一句 tool failed。不同结果会改变模型和 Runtime 的下一步:
| 情况 | Runtime 观察 | 合理后续 |
|---|---|---|
| 参数不合法 | invalid_arguments;函数未执行 |
修正参数或换工具 |
| 参数合法但服务超时 | execution_error,可重试 |
重试、降级或报告阻塞 |
| 搜索完成但无匹配项 | 成功结果 items: [] |
换查询策略或标记资料缺口 |
| 返回值不符合 Output Contract | invalid_tool_result |
修复工具实现,不能回填畸形数据 |
空数组是业务结果,不是错误。将它伪装成超时会让系统盲目重试;将超时伪装成空结果又会让模型错误得出“没有官方资料”。错误类型应来自 Runtime 对执行事实的记录,不应由模型根据一段文字猜测。
Tool Result 回到 Context,才成为 Observation
搜索成功后,Runtime 不应只在本地日志写下结果。它必须将规范化的结果与原 call_id 放进下一次 Request:
{
"type": "tool_result",
"call_id": "call_01",
"result": {
"ok": true,
"data": {
"items": [
{
"title": "Sessions",
"url": "https://example.test/openai-agents/session",
"source_type": "official",
"snippet": "Sessions provide a persistent memory layer..."
}
]
}
}
}
对 Runtime 来说这是 Tool Result;对下一次模型推理来说,这是新的 Observation。模型现在才有条件决定是否读取该 URL、提取哪个维度,或继续寻找资料。若结果只存在日志而未加入新的 Request,模型根本看不到它,往往会重复调用同一个工具。
Observation 也不等于已验证的事实。搜索摘要会截断,网页会更新,工具返回还可能含有不可信内容。C04 只建立“获得并回填外部观察”的通道;证据的可信度、任务的验收和完成判定仍服从 C03 的契约,后文再展开。
C04 的交付:一条受控的行动链,而不是完整 Loop
下面这段 Trace 将“模型提出调用”和“工具确实执行”分开记录:
case=C04 task_id=task_c04 event=model.response
call_id=call_01 tool=search_sources
event=tool.arguments_validated status=passed
event=tool.execution_started
event=tool.execution_completed item_count=2 duration_ms=18
event=tool.result_validated status=passed
event=tool.result_attached call_id=call_01
case=C04 event=model.response
call_id=call_02 tool=fetch_document
event=tool.execution_completed chars=8421 truncated=false
event=tool.result_attached call_id=call_02
case=C04 event=model.response
call_id=call_03 tool=save_evidence
event=tool.authorization_checked task_scope=matched
event=tool.execution_completed evidence_id=ev_001 stored=true
再以 limit: 50、后端超时和“无匹配项”各跑一次,Trace 应分别出现参数拒绝、执行失败和 item_count=0。这样,系统能回答四个不同的问题:模型请求了什么?Runtime 是否允许?真实函数是否运行?模型最终看到了什么?
C04 的边界到此为止。它没有自动规划,也没有连续执行,更没有宣布调研完成。它只是让 C03 中已准备好的 Task 第一次安全地接触外部世界,并把观察带回模型。
Function Calling 与 MCP 分别解决什么
Function Calling 是模型 API 与应用之间的交互格式:应用提供工具定义,模型生成调用,应用执行后提交结果。函数可以是本地代码,也可以封装数据库或远程服务。
MCP 则标准化应用怎样从外部服务器发现、调用工具并取得结果。它可以替换本篇 Registry 与远程工具之间的接线,却不会替 Host 决定向模型开放哪些工具、是否授权、如何处理超时或什么结果应进入 Context。接入 MCP 不会自动得到一个 Agent Loop。
小结
前三篇分别回答“模型一次能做什么”“应用怎样管理一次调用”“系统对用户承诺什么”。第四篇补上执行层的第一个接口:模型以 Tool Call 提出行动建议,Runtime 通过契约、校验与授权执行真实工具,再以 Tool Result 把 Observation 带回下一次推理。
Task Contract → Executable Task → Request + Tool definitions
↓
Tool Call(模型建议)
↓
Runtime 校验、授权、执行
↓
Tool Result / Observation
现在还缺少一个决定者:拿到 Observation 后,谁判断该继续搜索、读取、保存,还是停止?下一篇才进入第一个真正的 Agent Loop。
案例实践清单
- 为三个工具定义 Name、Description、Input Schema 和 Output Schema。
- Runtime 只执行 Registry 中已开放且被授权的工具。
- 区分参数失败、执行失败、空业务结果和无效输出。
- 用
call_id将每个 Tool Result 关联回原 Tool Call。 - 在结果进入下一次 Request 前校验 Output Contract。
- 限制
save_evidence只能写入当前 Task 的受限空间。 - 记录可区分“请求”“执行”“回填”的 C04 Trace。
参考资料
- OpenAI:Function calling:Tool Call、函数执行、
call_id与结果回填的流程。 - Anthropic:Define tools:工具定义、Description 写法与
tool_use/tool_result模式。 - Model Context Protocol:Tools:工具发现、调用、Input/Output Schema 与结构化结果。
- JSON Schema:Object reference:对象字段、必填属性与
additionalProperties等基础约束。