面向 LLM 的程序设计 6:Tool Calling 的完整生命周期——从定义、决策、执行到观测回注

当模型不再只「说话」,而是调用外部能力 (查库、下单、跑脚本)时,工程上真正运转的是一整条 Tool / Function Calling 生命周期 。只在聊天里贴一段工具 JSON,往往不够:谁维护定义何时注入参数错了怎么办结果怎么写回对话在哪里记日志,都会在生产里放大成稳定性与安全问题。

本篇为系列第六篇 :前五篇从 REST 能力化Schema响应形状版本化超媒体 把「HTTP 这一侧的接口」铺平;本篇转向 「模型 ↔ 工具」编排闭环 ,用同一套词汇把各阶段对齐,并给出可运行 demo(真实大模型 通过 Chat Completions 发起 tool_calls + 真实 HTTP 工具端点 + 回注后再推理)。

摘要 :完整链路可分为 定义上下文注入模型决策(是否调用、调用谁)参数解析与校验执行(HTTP/RPC/本地)观测与结果回注继续推理 。每一环都应有明确失败策略:非法参数在执行前 拦截;执行失败返回结构化 tool 消息 ;与 JSON Schema、幂等、错误体 (系列其他篇)衔接。配套代码用 OpenAI 风格工具列表LangChain ChatOpenAI.bind_tools (与 LangGraph Tool Use 示例同思路)、jsonschema 编排侧预校验FastAPI 工具服务 串起主路径,并附录纯本地校验失败分支。

关键词:Tool Calling;Function Calling;生命周期;JSON Schema;编排;观测;tool 消息;Agent

代码链接:Tool Calling 的完整生命周期示例代码


1 为什么需要「生命周期」视角?

1.1 单点优化容易漏网

只把 description 写长,不解决 注入过大导致选错工具 ;只在服务端 422,不在编排层提示模型,会 重复无效重试 ;只打 HTTP 访问日志,不关联 tool_call_id ,排障时对不上是哪一次模型决策

1.2 拆解链路

想象一下流水线质检:每一站放行标准清晰,坏件不会默默流到下一站。

阶段 白话 常见落点
1. 定义 工具叫什么、干什么、参数长什么样 仓库内 JSON / OpenAPI 生成 / MCP tools/list
2. 注入 当前轮对话里模型看得见哪些工具 System prompt、API tools 字段、动态筛选
3. 决策 模型输出是否含 tool_calls、选哪个 name 各厂商 chat/completions 协议
4. 解析与校验 arguments 字符串 → JSON → Schema 编排器、执行前
5. 执行 真实 IO:HTTP、DB、沙箱 网关、服务实现;鉴权、幂等、超时
6. 观测 Trace、审计、耗时、成本 tool_call_id、span 关联
7. 回注与再推理 role: tool 写回消息列表 多轮 API;LangGraph 节点

💡 理解要点第 4 步 是「编排器防线」,第 5 步 是「服务真相」------两边契约应 同源,否则会出现编排器放行、服务仍 422 的漂移。


2 各阶段设计要点(精简清单)

2.1 定义(与系列第 2、7 篇呼应)

  • name :稳定、唯一,避免 search / search_v2 混用又无版本策略。
  • description做什么、何时用、与相邻工具的区别------比堆参数更减误选。
  • parameters :JSON Schema,additionalProperties: false 在多数场景利于早失败。

🔍 简例 :订单场景下同时需要「单笔详情」与「某人订单列表」,可定义两个工具,避免共用一个模糊 name。下面是与常见 Chat Completions「tools」字段兼容的 JSON 片段(字段名以各厂商最新文档为准):

json 复制代码
[
  {
    "type": "function",
    "function": {
      "name": "get_order_by_id",
      "description": "在已掌握明确 order_id 时查询单笔订单详情。不要用本工具按用户 id 猜测或枚举单号。",
      "parameters": {
        "type": "object",
        "properties": {
          "order_id": {
            "type": "string",
            "description": "订单唯一标识"
          }
        },
        "required": ["order_id"],
        "additionalProperties": false
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "list_orders_by_user",
      "description": "列出某用户名下多条订单。与 get_order_by_id 不同:本工具不接受单独 order_id,只接受 user_id 与筛选条件。",
      "parameters": {
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string",
            "description": "用户唯一标识"
          },
          "status": {
            "type": "string",
            "enum": ["pending", "paid", "shipped", "cancelled"],
            "description": "按状态筛选;不传则返回全部状态"
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50,
            "default": 10,
            "description": "最多返回条数"
          }
        },
        "required": ["user_id"],
        "additionalProperties": false
      }
    }
  }
]

模型在「用户 id + 想看有哪些单」与「手里已经有一条单号」两种意图下,更容易选对工具;多填了未声明字段时,在第 4 步 就会被 Schema 拦下。与本案例 里 echo_message / add_integers 用不同 description 区分边界,是同一思路。

2.2 分层注入

  • 工具多时分批、按意图检索、或分层(「常用工具包」+「按需拉取」)。
  • 注入内容应与 OpenAPI / 运行时路由 同步版本(参见第 4 篇)。

🔍 简例 :假设平台一共注册了 48 个工具(订单、售后、商品、运营后台......),若每轮请求把 完整 name + description + parameters 全塞进 tools,系统提示里还要带业务规则,上下文会很快被「工具说明书」占满,模型也更容易在相近工具之间选错。

一种常见做法是 分层注入

  1. 常驻小包 :每轮只注入 3~8 个真正通用的工具(例如 get_order_by_idlist_orders_by_user、企业内部 search_kb),保证基础能力始终可用、Token 有上限。
  2. 按轮按需扩容 :根据上一轮用户话轻量意图路由 (关键词、小分类模型、规则)再追加一组,例如用户提到「退款、售后单」时,再注入 create_refund_requestget_return_label 等;未命中则不注入,避免无关 description 干扰选型。
  3. 版本对齐 :生成 tools 列表的流水线与 openapi.json / 网关路由 共用同一 Git 修订或 api_version 标签 ;发版时若已将路径升为 /v2/...禁止 出现「工具 JSON 里还是 POST /v1/...」而路由已切到 v2 的半截子迁移。

下面是一段编排层伪代码示例:

python 复制代码
CORE_TOOLS = ["get_order_by_id", "list_orders_by_user", "search_kb"]

def tools_for_this_turn(user_text: str, registry: dict) -> list:
    names = list(CORE_TOOLS)
    if any(k in user_text for k in ("退款", "售后", "退货")):
        names += ["create_refund_request", "get_return_label"]
    return [registry[n] for n in names if n in registry]

真实场景下,我们还会故意缩短发给模型的参数说明 :Prompt 里往往只放每个字段的一行摘要,不把整份 JSON Schema 全贴进去,这样能再省一截 Token。注意两点 :一是缩略版里仍要让模型看清哪些必填、各字段大致什么类型 ,否则它会乱填;二是真正执行工具时 (下文第 4 步或你的服务端)仍用完整 Schema 做校验与兜底,别把「给模型看的简版」当成唯一真相。

🔍 简例(参数说明裁剪) :假设有工具 search_orders服务端/registry 里 可以是完整 JSON Schema:例如 statusenum: ["pending","shipped","delivered"]pageminimum: 1keywordmaxLength: 100 等。塞进 Prompt 给模型看的 可以缩成几行文字:keyword(必填,字符串)、status(选填,pending / shipped / delivered)、page(选填,整数页码)。

2.3 决策与解析

  • 多工具并行调用时,编排器需决定 顺序、依赖、是否允许并行

    🔍 简例(并行 vs 顺序) :用户说「查一下订单 ORD-9 的详情,再列出下单人名下还在配送中的单」。模型可能一次返回两个 tool_callsget_order_by_idorder_id: "ORD-9")和 list_orders_by_user(需要 user_id)。若 user_id 只能来自上一笔详情的响应 ,编排器应识别 数据依赖 :先 串行 执行 get_order_by_id,从 tool 结果里取出 buyer_user_id,再 注入或补全 第二个调用的参数(或在第二轮对话里再调 list_orders_by_user),而不是把两个 HTTP 请求 无次序并行 打出去导致第二个缺参。反之,用户只说「同时查北京和上海明天天气」且两工具互不依赖,编排器可 并行 调用以压低延迟。

  • arguments 必须是合法 JSON 字符串 (部分模型会漏引号);要有 修复或报错 策略,勿静默当空对象。

    🔍 简例(arguments 解析) :工具参数在协议里通常是一段 字符串 ,里面应能 json.loads 成对象。模型有时会少写 英文双引号 ,交出来的就不是合法 JSON。例如它想表达「message 的值是中文你好」,却写成了:

    text 复制代码
    {"message": 你好}

    在 JSON 里,字符串类型的值必须用双引号包起来你好 两边没有 ",解析器会把 你好 当成「莫名其妙的记号」而不是字符串,因此 json.loads 直接报错正确写法应是:

    json 复制代码
    {"message": "你好"}

    真实场景下可以:(1) 直接向模型回一条 tool 侧错误,要求重新输出合法 JSON;(2) 在受控场景下尝试轻量修复(如给未加引号的中文字段值套引号)------但修复规则要写清边界,避免把恶意内容「修成合法」。不要 在解析失败时默默当作 {} 去调接口,否则会出现「工具被调了但参数全空」的难查故障。

2.4 校验失败 vs 执行失败

  • 校验失败 :不发起外部 IO,向模型返回 简短、可行动 的错误(缺哪个字段、类型不对)。
  • 执行失败 :HTTP 5xx、超时;应带 是否可重试request_id(系列后续「错误体」篇)。

🔍 简例 :模型选了 add_integers ,但 arguments 解析后是 {"a": 1}少了必填的 b 。编排器在 JSON Schema 校验 阶段就失败------此时 不应POST /tools/add_integers 发请求(没有合法参数可发),而应构造一条 role: tool 回注,让模型知道「缺什么、怎么改」,例如 content 里放:

json 复制代码
{
  "ok": false,
  "error_kind": "validation_failed",
  "tool_name": "add_integers",
  "message": "缺少必填字段 b(integer)",
  "retryable": false
}

若参数完整且合法,请求已发出,服务端返回 502 Bad Gateway客户端读超时 ,则属于 执行失败 :业务上「加法的入参」并没写错,问题在依赖服务或网络。此时回注里更适合带 retryable: true (若你们的退避策略允许)、request_id(与网关日志对齐),并避免用「请检查 a、b 是否为整数」这类话术------否则会误导模型反复改一个本来正确的参数。

2.5 Trace/日志

  • 日志与 Trace 建议带:conversation_id、tool_call_id、工具名、耗时、状态
  • 回注内容 优先结构化 JSON 字符串,便于下一轮模型 少幻觉、少编造字段

🔍 简例(观测) :一次工具调用结束后,除业务库流水外,建议在日志或 Trace 上至少能拼回「谁、在什么对话里、调了哪个工具、花多久、成败」。例如一条 结构化日志(字段名按你们规范即可):

json 复制代码
{
  "event": "tool_call_finished",
  "conversation_id": "conv_8f3a",
  "tool_call_id": "call_demo_1",
  "tool_name": "get_order_by_id",
  "status": "success",
  "duration_ms": 47,
  "http_status": 200
}

今后追溯起来可用 tool_call_id 把「模型那一跳的 assistant 消息」与「网关访问日志、下游 request_id」串起来。

🔍 简例 :同上订单查询,HTTP 成功体不要只把自然语言塞进 content;更稳妥是把完整 JSON (可含 _links )作为字符串交给 role: tool,下一轮模型既能读到 order_idstatus,也能读到「下一步可点的链接」。例如:

json 复制代码
{
  "role": "tool",
  "tool_call_id": "call_demo_1",
  "name": "get_order_by_id",
  "content": "{\"response_type\":\"order_detail\",\"order_id\":\"ORD-9\",\"status\":\"shipped\",\"_links\":{\"self\":{\"href\":\"https://api.example.com/orders/ORD-9\",\"method\":\"GET\"},\"list_by_user\":{\"href\":\"https://api.example.com/users/u_102/orders?status=shipped\",\"method\":\"GET\"}}}"
}

外层是发给聊天 API 的消息;content 字符串 解析后才是业务负载。若只回注一句「订单已发货」,模型后续容易编造 运单号或链接;结构化 + _links 可把「事实」与「允许的下一步」绑在一起。(工具选型、参数校验的简例见上文 2.1、2.3、2.4。)


3 案例分析

3.1 背景与目的

第 1、2 节把 Tool Calling 拆成阶段、列清单 ;真正难的是 各阶段在实现里如何对齐、如何交接 。本案例刻意把业务压到最小:只保留两个能力------回显一段文字两整数求和 ------并配一个可在本机访问的 HTTP 工具服务,使你不必分心于领域细节,就能看清整条链路。

与「只在本地进程里用函数当工具」的常见写法不同,这里把工具的真实执行 放在 独立的 Web 接口 上:编排侧在发请求前先做 与工具定义同源的参数校验 ,通过后再访问远端;这样更贴近生产里常见的 网关、微服务或异构后端 ,也能区分清楚:编排层的早失败服务端的入参校验是两层不同的防线。

案例覆盖从 定义与可见性 一直到 回注与再推理,并与第 1 节表中的各阶段一一对应;其中「再推理」在运行输出里单独标成一步,以免和「观测、日志」混为一谈。

3.2 先决条件与配置

项目 说明
Python 建议 3.10+;需能安装 langchain-openailangchain-core
终端一 启动 uvicorn server_api:app(默认 127.0.0.1:8315),工具 HTTP 必须可达。
终端二 python main.py;可传自定义用户句:python main.py "请计算 17+25"
密钥与模型 demo/.env 或环境中配置 OPENAI_API_KEYDASHSCOPE_API_KEY ;兼容 OpenAI API 时可配 BASE_URLMODEL 须支持 function calling(如 gpt-4o-miniqwen-plus)。
与工具服务对齐 API_BASE (或 API_HOST + API_PORT)须与 uvicorn 监听地址一致,否则阶段 5 会连接失败。

3.3 端到端数据流(与源码对应)

6-7 回注与再推理
5 执行
4 编排校验
3 模型决策
1-2 定义与注入
通过
失败
tool_definitions.py
ChatOpenAI bind_tools
jsonschema
httpx POST
server_api FastAPI
ToolMessage
ChatOpenAI 无 tools

  • 主路径 :校验通过 → HTTP → 业务 JSON → ToolMessage → 第二轮 LLM 文本回复。
  • 校验失败路径 :跳过 HTTP,ToolMessage.content 为结构化错误摘要,第二轮 LLM 仍可据此向用户说明「参数不合法」等(主路径附录为教学用本地片段,未自动触发第二轮)。

3.4 文件分工(与阶段对齐)

文件 职责 主要对应阶段(见第 1 节表)
tool_definitions.py OpenAI 风格工具列表、name → POST 路径、按 name 取参数 Schema 1 定义;2 注入数据源;4 校验 Schema
config_parser.py API_BASEOPENAI_API_KEY / DASHSCOPE_API_KEYBASE_URLMODEL 运行前置条件
prompt.py Jinja2 系统提示模板 TOOL_LIFECYCLE_SYSTEM_PROMPT_TEMPLATErender_tool_lifecycle_system_prompt() 3 / 7SystemMessage 文案来源
server_api.py POST /tools/echo_messagePOST /tools/add_integers,返回 response_type: tool_result 5 执行(服务端)
main.py 阶段 1--7 打印与编排;附录本地校验失败 2 注入展示 → 3 真实决策 → 4 校验 → 5 客户端 HTTP → 6 回注展示 → 7 再推理

3.5 各阶段含义、相关代码与案例输出

编排入口在 run_lifecycle_demo:依次调用各 _phase_*_phases_validate_execute_observe,最后跑附录。所有面向终端的阶段性正文都经 _emit 输出;若在 Notebook 里配置了 logging,同一内容会同步出现在 INFO 日志中。

整体打印顺序:阶段 1 → 2 → 3 →(阶段 4 与 5 的标题各打一次)→ 对每个 tool_call 循环打印 4/5 的细节 → 阶段 6 → 7 → 附录 。阶段 2 的注入 JSON 超过约 600 字符会 ...(截断)...阶段 3、7 的原文随模型与温度变化 ;下文的「案例输出」中,tool_call_id、自然语言总结可能与你的机器不一致,但结构 应与当前 main.py 一致。


阶段 1:工具定义(对应第 1 节「1. 定义」)

是什么 :把「有哪些工具、各自干什么、参数长什么样」固定成一份可被模型消费的元数据。案例里等价于浏览注册表:只打印每个工具的 namedescription 前 50 字,避免终端被长 Schema 淹没。

相关代码tool_definitions.py 中的列表 TOOLS_FOR_LLM_CONTEXT(OpenAI 风格的 type: function 项);main.py_phase_definitions() 遍历该列表并 _emit

py 复制代码
def _phase_definitions() -> None:
    _emit("【阶段 1】工具定义(注册表 / OpenAPI / MCP 同源为佳)\n")
    for t in TOOLS_FOR_LLM_CONTEXT:
        fn = t["function"]
        _emit(f"  - {fn['name']}: {fn['description'][:50]}...")

工具定义如下:

py 复制代码
TOOLS_FOR_LLM_CONTEXT: list[dict[str, Any]] = [
    {
        "type": "function",
        "function": {
            "name": "echo_message",
            "description": (
                "回显一条文本。用于演示工具调用链路与观测日志。"
                "当用户要求重复或确认某句话时使用。"
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "message": {
                        "type": "string",
                        "description": "要回显的文本",
                    },
                },
                "required": ["message"],
                "additionalProperties": False,
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "add_integers",
            "description": (
                "计算两个整数的和。仅在用户明确需要加法时使用;"
                "与 echo_message 不同,不要用于纯重复文本。"
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "a": {"type": "integer", "description": "第一个加数"},
                    "b": {"type": "integer", "description": "第二个加数"},
                },
                "required": ["a", "b"],
                "additionalProperties": False,
            },
        },
    },
]

案例输出(示意)

复制代码
【阶段 1】工具定义(注册表 / OpenAPI / MCP 同源为佳)

  - echo_message: 回显一条文本。用于演示工具调用链路与观测日志。当用户要求重复或确认某句话时使用。...
  - add_integers: 计算两个整数的和。仅在用户明确需要加法时使用;与 echo_message 不同,不要用于纯重复文本...

阶段 2:上下文注入

(注意,阶段2实际上并不是真的向llm注入了这几个工具,而是作为演示打印出来作为demo使用。真正意义上的注入在阶段 3)

把阶段 1 那份工具定义送进「当前轮模型看得见」的上下文。实现上与 Chat Completions 的 tools 字段同源;案例里用 json.dumps 打印整段 JSON(过长则截断),真实请求里则由 bind_tools(TOOLS_FOR_LLM_CONTEXT) 带上同一批定义。

相关代码main.py_phase_inject_context();与阶段 3 共用的数据源仍是 TOOLS_FOR_LLM_CONTEXT

py 复制代码
def _phase_inject_context() -> str:
    _emit("\n【阶段 2】上下文注入 --- 将工具列表写入 API 请求的 tools 字段(与写入 system 互补)\n")
    payload = json.dumps(TOOLS_FOR_LLM_CONTEXT, ensure_ascii=False, indent=2)
    truncated = payload if len(payload) <= 600 else payload[:600] + "\n...(截断)..."
    _emit(truncated)
    return payload

案例输出(示意):先出现阶段标题,接着是数组形式的工具 JSON(可能被截断):

复制代码
【阶段 2】上下文注入 --- 将工具列表写入 API 请求的 tools 字段(与写入 system 互补)

[
  {
    "type": "function",
    "function": {
      "name": "echo_message",
      "description": "...",
      "parameters": { ... }
    }
  },
  ...
...(截断)...

阶段 3:模型决策

由支持函数调用的对话模型根据系统提示与用户话,决定本轮是否输出 tool_calls、调用哪个 name、参数是什么。案例用 ChatOpenAI 绑定工具后 invoke;返回的 AIMessage 被转成接近原生 API 的 JSON 再打印,便于和文档里的 assistant 消息对照。

相关代码prompt.py 中 Jinja2 模板 TOOL_LIFECYCLE_SYSTEM_PROMPT_TEMPLATErender_tool_lifecycle_system_prompt()main.py_system_prompt()DEFAULT_USER_QUERY(默认用户句)、_build_llm(bind_tools=True)_phase_model_decision()_assistant_to_api_style_dict()。模型与密钥来自 config_parser.demo_config.env 中的 MODELBASE_URL、Key 等)。

py 复制代码
def _phase_model_decision(user_text: str) -> AIMessage | None:
    _emit("\n【阶段 3】模型决策 --- Chat Completions + tools,由真实模型产出 tool_calls(或纯文本)\n")
    llm_tools = _build_llm(bind_tools=True)  # 注意,这里才是真正向llm注入了这几个工具
    messages = [
        SystemMessage(content=_system_prompt()),
        HumanMessage(content=user_text),
    ]
    ai = llm_tools.invoke(messages)
    if not isinstance(ai, AIMessage):
        _emit("  非预期返回类型,跳过。")
        return None
    _emit(json.dumps(_assistant_to_api_style_dict(ai), ensure_ascii=False, indent=2))
    return ai

对应的 system prompt 内容如下:

py 复制代码
TOOL_LIFECYCLE_SYSTEM_PROMPT_TEMPLATE = """你是 Tool Calling 生命周期演示中的助手。当前轮 API 会注入若干 function 工具,你只能在确有需求时调用,且参数必须符合类型与必填约束。

- echo_message:仅在用户要你回显、重复或确认一段给定文字时使用;参数 message 为要回显的字符串。
- add_integers:仅在用户明确要求计算两个整数之和时使用;参数 a、b 必须为整数。不要用本工具完成「只回显文字」的需求。

若同一用户请求中同时需要回显与加法,可按需发起多次工具调用。收到工具返回的 JSON 后,先用简短中文向用户说明结果。
{%- if extra_system_instructions %}

{{ extra_system_instructions }}
{%- endif %}
"""

案例输出(示意) :若模型选择调用 echo_message,大致为:

复制代码
【阶段 3】模型决策 --- Chat Completions + tools,由真实模型产出 tool_calls(或纯文本)

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_............",
      "type": "function",
      "function": {
        "name": "echo_message",
        "arguments": "{\"message\": \"超媒体与工具链,你好。\"}"
      }
    }
  ]
}

若模型不调用工具,则常见为 "content": "......" 且没有 tool_calls 字段(或 tool_calls 为空);后续阶段 4--7 会走「跳过或仅说明」分支。


阶段 4:解析与校验

把模型给出的每个 tool_call 统一成 (name, args 字典, tool_call_id)_normalize_tool_call),再用与工具 parameters 同源的 JSON Schema 做 执行前 校验。未知工具名或校验失败时不发起 HTTP ,改为生成带 response_type: tool_errorToolMessage,与成功路径的回注形状一致,便于下一轮模型消化。

相关代码main.py_normalize_tool_callparameters_schema_by_tool_name()(来自 tool_definitions.py)、Draft202012Validator.validate;逻辑位于 _phases_validate_execute_observefor tc in decision.tool_calls 循环前半段。

案例输出(示意) :阶段 4、5 的标题 在循环外各打印一次;随后每个 tool_call 会打印分隔行、解析后参数校验通过校验失败: ...(不发起 HTTP)。成功时例如:

复制代码
【阶段 4】参数解析与 JSON Schema 校验(编排器侧,执行前)


【阶段 5】执行 --- HTTP 调用工具端点(可在此插入审计、限流、幂等键)

  --- tool_call_id=call_............ name=echo_message ---
  解析后参数: {'message': '超媒体与工具链,你好。'}
  校验通过: echo_message({'message': '超媒体与工具链,你好。'})

(下一行 POST /tools/... 属于阶段 5,见下。)


阶段 5:执行(对应第 1 节「5. 执行」)

是什么 :校验通过后,按 TOOL_HTTP_PATH 将工具名映射为 POST 路径,用 httpxargs 作为 JSON 发往本机 FastAPI。HTTP 异常时同样写入 tool_errorToolMessage,不抛死导致编排中断。

相关代码tool_definitions.pyTOOL_HTTP_PATHserver_api.pyPOST /tools/echo_messagePOST /tools/add_integers 及 Pydantic 请求体;main.py 循环内的 httpx.Client.post

案例输出(示意)(接在阶段 4 示例之后):

复制代码
  POST /tools/echo_message -> {'response_type': 'tool_result', 'tool_name': 'echo_message', 'ok': True, 'echoed': '超媒体与工具链,你好。'}

阶段 6:观测与回注(对应第 1 节「6. 观测」与「7. 回注」中的回注部分)

是什么 :把每个执行结果封装成等价于 Chat API role: tool 的结构并打印:tool_call_id 与 assistant 里同 id 对齐,name 为工具名,content字符串形式的 JSON (成功时为服务端返回体,失败时为 tool_error)。生产里此处往往同时写审计日志、Trace;案例只演示终端可读的「回注载荷」。

相关代码main.py_phases_validate_execute_observe 末尾对每个 ToolMessage 构造 printable 字典并 _emit

案例输出(示意)

复制代码
【阶段 6】观测与回注 --- 将 ToolMessage 写入消息列表(等价于 API 的 role: tool)

{
  "role": "tool",
  "tool_call_id": "call_............",
  "name": "echo_message",
  "content": "{\"response_type\": \"tool_result\", \"tool_name\": \"echo_message\", \"ok\": true, \"echoed\": \"超媒体与工具链,你好。\"}"
}

多个 tool_call 时会打印多段类似 JSON。


阶段 7:回注后再推理(对应第 1 节「7. 回注与再推理」中的再推理)

是什么 :在已有「用户消息 → assistant(含 tool_calls)→ 若干 tool 消息」的链上,再调用同一模型但不绑定 tools ,生成面向用户的自然语言总结。案例里历史为:SystemMessage + HumanMessage + 首轮 AIMessage + 全部 ToolMessage

相关代码main.py_phase_follow_up_reply();若 tool_msgs 为空(例如无 tool_calls)则该函数直接返回,不会打印阶段 7 标题。

py 复制代码
def _phase_follow_up_reply(user_text: str, decision: AIMessage, tool_msgs: list[ToolMessage]) -> None:
    if not tool_msgs:
        return
    _emit("\n【阶段 7】回注后再推理 --- 第二轮 chat(不绑定 tools),生成面向用户的自然语言总结\n")
    llm_plain = _build_llm(bind_tools=False)
    history = [
        SystemMessage(content=_system_prompt()),
        HumanMessage(content=user_text),
        decision,
        *tool_msgs,
    ]
    final = llm_plain.invoke(history)
    content = getattr(final, "content", None) or ""
    _emit(content.strip() or "(空 content)")

案例输出(示意)

复制代码
【阶段 7】回注后再推理 --- 第二轮 chat(不绑定 tools),生成面向用户的自然语言总结

echo_message 已按你的要求回显了「超媒体与工具链,你好。」,链路正常。

附录:纯本地校验失败(教学分支,不对应主链路某一「运行时」阶段)

是什么 :主路径跑完后,额外用一组写死的非法参数add_integers 的 Schema 做一次校验,演示「编排层先于 HTTP 拦截」时终端长什么样;不调用模型、不访问工具服务

相关代码main.py_demo_validation_failure();Schema 仍来自 parameters_schema_by_tool_name()

案例输出(示意)

复制代码
======== 附录:纯本地校验失败分支(不经过大模型、不发起 HTTP)========

add_integers 非法参数: 'not_int' is not of type 'integer'
实务:若该参数来自某次 model tool_calls,应把同类摘要写入 role: tool,避免无效重试。

若模型未发起任何 tool_calls ,你会看到阶段 4、5 标题后出现「本轮无 tool_calls...」,若有纯文本则附「模型直接文本回复(摘录)」;阶段 6 无 JSON 块,阶段 7 不执行。可调整系统提示、用户句或换用更强 tool 模型。生产侧还需处理超时、429、部分成功、工具循环上限等,见 第 4 节 链接文档。

4 完整代码与文档

design-for-ai-invocation/06_tool_calling_lifecycle/demo/ 下安装依赖后,终端一启动工具服务:

bash 复制代码
uvicorn server_api:app --reload --host 127.0.0.1 --port 8315

终端二运行编排脚本:

bash 复制代码
python main.py

config_parser.envAPI_BASEOPENAI_API_KEY / DASHSCOPE_API_KEYBASE_URLMODELAPI_BASE 须与 uvicorn 监听地址、端口一致。)

相关推荐
智星云算力2 小时前
本地GPU与租用GPU混合部署:混合算力架构搭建指南
人工智能·架构·gpu算力·智星云·gpu租用
jinanwuhuaguo2 小时前
截止到4月8日,OpenClaw 2026年4月更新深度解读剖析:从“能力回归”到“信任内建”的范式跃迁
android·开发语言·人工智能·深度学习·kotlin
xiaozhazha_2 小时前
效率提升80%:2026年AI CRM与ERP深度集成的架构设计与实现
人工智能
枫叶林FYL2 小时前
【自然语言处理 NLP】7.2.2 安全性评估与Constitutional AI
人工智能·自然语言处理
AI人工智能+2 小时前
基于高精度身份证OCR识别、炫彩活体检测及人脸比对技术的人脸核身系统,为通信行业数字化转型提供了坚实的安全底座
人工智能·计算机视觉·人脸识别·ocr·人脸核身
小敬爱吃饭2 小时前
Ragflow Docker部署及问题解决方案(界面为Welcome to nginx,ragflow上传文件失败,Docker中的ragflow-cpu-1一直重启)
人工智能·python·nginx·docker·语言模型·容器·数据挖掘
宸津-代码粉碎机2 小时前
Spring Boot 4.0虚拟线程实战调优技巧,最大化发挥并发优势
java·人工智能·spring boot·后端·python
老兵发新帖3 小时前
Hermes:比openclaw更好用的智能体?
人工智能
俊哥V3 小时前
每日 AI 研究简报 · 2026-04-09
人工智能·ai