工具调用——让模型把答案"填进表格"而不是"写在作文里"

系列第 3 篇。前两篇都属于"文本派":模型用自然语言作文,我们事后解析。这一篇换思路------既然平台提供了"工具调用"这种机制,为什么不让模型直接把结构化答案填进去?

两个问题先想明白

为什么绑定工具,模型就按工具的结构生成?

你给模型注册一个"函数"(含名字、描述、参数 schema),模型如果决定调用它,参数就必须符合 schema。这不是玄学,是两个层面兜底:

  1. 训练层面:现役模型在训练/对齐时见过海量 function-calling 数据,学会了"调用函数时参数要符合该函数的参数声明"。
  2. 平台解码层面(更硬) :工具调用在 API 里被编码成特殊的 token 流 。模型一旦进入工具调用模式,解码器就走专门的语法路径,保证 arguments 是合法 JSON;更强力的平台还会按 schema 在解码端校验。

所以"按结构生成"是训练能力 + 平台语法强制的共同结果,不是模型大发慈悲。

为什么工具不需要真实提供?

因为 function calling 是"接口协议",不是"执行通道" 。你把 {name, description, schema} 发给模型,语义只是"存在一个长这样的函数,你可以生成对它的调用"。模型生产的是一段"调用描述"(函数名 + 参数),它从不检查这函数是否真的被你实现。

于是诞生一个骚操作:注册一个假的工具,纯粹当 JSON 的容器 。模型返回的 tool_calls 里,参数就是你要的结构化答案------要不要真的去执行它,由你决定。

arduino 复制代码
你想让它执行真实功能 ──→ tools 接到搜索/数据库/文件操作
你只想要结构化答案 ────→ 把工具当"表格",取走 tool_calls.args 就行

手写 bindTools:把 schema 变成工具

LangChain 里注册工具最简单的方式是 bindTools

js 复制代码
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";

const model = new ChatOpenAI({ /* modelName / apiKey / baseURL 照旧 */ });

const scientistSchema = z.object({
  name: z.string().describe("科学家的姓名"),
  birth_year: z.number().describe("出生年份"),
  nationality: z.string().describe("国籍"),
  fields: z.array(z.string()).describe("研究领域列表"),
});

// 把 schema 包装成一个"工具"注册给模型
const modelWithTool = model.bindTools([
  {
    name: "extract_scientist_info",
    description: "提取和结构化科学家的详细信息",
    schema: scientistSchema,   // zod → 自动转成参数 JSON Schema
  },
]);

const response = await modelWithTool.invoke("介绍一下爱因斯坦");

// 结构化答案在这
console.log(response.tool_calls[0].args);
// { name: "爱因斯坦", birth_year: 1879, nationality: "德国",
//   fields: ["理论物理学", "相对论", "量子力学"] }

注意几点:

  1. tool_calls[0].args 天生就是对象 ,全程没有 JSON.parse、没有 ```````json```` 围栏、没有"哄"------因为平台把结构化数据放在了专门的参数槽位里。
  2. content 通常是空的------模型把力气都花在"调用工具"上了,没写作文。这正是"填表格"和"写作文"的区别。
  3. 顺带一个冷知识:OpenAI 原生响应里 arguments 其实是字符串 ,是 LangChain 帮你 parse 成对象的。所以两条路线的底层数据类型都是字符串,差别只在约束强度 ------content 那串没人管你写啥,arguments 那串被平台强制必须是合法 JSON。

一个必须知道的隐患:模型可能拒绝调用

默认的 tool_choiceauto------模型有权决定"我不调用工具,直接文字回答" 。一旦它这么干,response.tool_calls 就是 undefined,上面 tool_calls[0] 直接报 TypeError

想让它"不许空着手走文本通道",就把选择权拿掉:

js 复制代码
const modelWithTool = model.bindTools([...], {
  tool_choice: "required",   // 强制必须调用工具
  // 或点名:tool_choice: { type: "function", function: { name: "extract_scientist_info" } }
});

两条路线对比

文本解析派(前两篇) 工具调用派(本篇)
数据落点 content 作文文本 tool_calls[0].args 参数槽
约束力 靠 prompt 哄,可被打破 平台语法强制
格式干扰 围栏/废话/截断都可能 天然结构,无干扰
需要的动作 事后 JSON.parse + 校验 .args,几乎零解析
前置条件 模型/服务端支持 tools

小结

  • 工具调用把"结构化"从输出后猜 挪到了请求时预订
  • 工具是接口协议,模型只生成"调用描述",不需要你真去实现------所以能当纯 JSON 容器用;
  • 手写版是 bindTools + 读 tool_calls[0].args,默认 auto 下记得补 tool_choice

但手写这套还是有重复劳动:抽 args、判空、可能还要校验。下一篇的正主 withStructuredOutput 就是把这些封装成一行------但封装背后藏着一个会让兼容服务直接 400 的默认值,务必看完再动手。

相关推荐
不好听6131 小时前
StructuredOutputParser——别再靠"哄",把 schema 讲清楚再验收
llm
武子康2 小时前
从声学信号到工具阻断:实时语音安全决策门的系统设计
人工智能·llm·agent
tachibana23 小时前
Embedding 有哪几种算法?
人工智能·算法·ai·大模型·llm·embedding·agent
吴佳浩12 小时前
FDE:从系统落地工程师,演变为企业 AI 能力的知识架构师
人工智能·llm·ai编程
吴佳浩12 小时前
从 OpenClaw、Codex 到 Hermes,看懂 AI Agent 架构为什么正在收敛
人工智能·llm·agent
stereohomology13 小时前
Wolfram会以什么姿态拥抱LLM
llm
掰头战士19 小时前
从LLM到Agent、Agent的6大核心。这些基础知识你还记得吗
node.js·llm·agent
甜辣uu19 小时前
智能体Agent性能优化从原理到实战
人工智能·性能优化·大模型·llm·agent·rag·智能体
XLYcmy21 小时前
Self-Adapting Language Models论文分享
自然语言处理·llm·微调·sft·论文笔记·强化学习·自进化