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。

参考资料