1604 字
约 5 分钟
20
FunctionCalling工具调用
无标签

Function Calling:给模型装上手脚

系列:ToyAgent 智能体知识库 04/26

一句话核心:模型负责决定"用什么工具、传什么参数",执行永远在你手里——这是智能体安全性的基石。

为什么需要

ReAct 教学版用提示词约定"让模型输出一段 JSON 文本",能用但脆弱:模型偶尔输出格式漂移、无法表达参数 schema、没有专门的校验环节。Function Calling 把"模型想调用工具"这件事从自由文本升级为协议级结构:请求体里声明工具清单(含 JSON Schema),模型在响应里返回结构化的调用请求,你来执行并回填。分工由此清晰——模型只出主意,动手的永远是你。

核心机制:三步舞

// 第一步:声明工具清单(最简 Tool Schema,生产版放请求体 tools 数组)
String SYSTEM_PROMPT = """
        你是一个可以调用工具的智能体。可用工具:
        - get_weather(city): 查询指定城市的实时天气
        - calculator(expression): 计算四则运算

        需要工具时,只输出 JSON:
        {"tool": "工具名", "arguments": {"参数名": "值"}}
        不需要工具时,直接自然语言回答。
        """;

// 第二步:模型返回结构化调用请求(只出主意,不执行)
String first = model.chat(messages);
// → {"tool": "get_weather", "arguments": {"city": "广州"}}

// 第三步:执行工具,把结果回填,模型基于事实作答
String result = execute(tool, args);
messages.add(Message.user("工具执行结果: " + result));
String answer = model.chat(messages);

完整链路:声明工具 → 模型决策调用 → 执行 + 回填 → 模型生成回答。

sequenceDiagram
    participant A as 应用
    participant M as 大模型
    participant E as execute 执行层
    A->>M: 请求体声明工具清单
    M-->>A: tool_calls get_weather 广州
    A->>E: 执行工具 工具名加参数
    E-->>A: 返回结果 26 度多云
    A->>M: 回填工具结果
    M-->>A: 生成最终自然语言回答

图示:Function Calling 三步舞——声明、决策、执行回填;模型只出主意,副作用永远发生在你的执行层。

安全性基石:决定权在模型,执行权在你

模型输出的只是一段文本(工具名 + 参数),它永远无法直接触达你的数据库或文件系统。所有副作用都发生在你的 execute() 里——你可以校验参数、做权限检查、加白名单。这是 Function Calling 与"让模型直接执行代码"的本质区别。

举例:一条生产化的执行链

在 execute() 这一层做校验、权限与可读错误,就是教学版与生产版真正的分水岭:

private String execute(String tool, Map<String, Object> args) {
    // 1. 工具白名单:模型幻觉出不存在的工具名时,返回可读错误而非异常
    if (!registry.contains(tool)) {
        return "错误:工具 " + tool + " 不存在。可用工具:" + registry.names();
    }
    // 2. 参数 schema 校验 + 权限检查(模型不能越过你的安全边界)
    if (!registry.checkPermission(tool, currentUser())) {
        return "错误:当前用户无权调用 " + tool;
    }
    try {
        return registry.invoke(tool, args);
    } catch (Exception e) {
        // 3. 失败要可读:返回模型能理解并自我纠错的文本,而不是抛堆栈
        return "执行失败:" + e.getMessage();
    }
}

模拟轨迹:

用户:广州今天多少度?穿什么合适?
模型 → tool_calls: get_weather({"city": "广州"})
execute() → "26°C 多云"
回填后模型 → 广州今天 26°C 多云,建议薄外套或长袖。

LangChain4j 里的等价写法——工具就是一个带注解的方法,框架替你完成 schema 生成与回填:

class WeatherTools {
    @Tool("查询指定城市的实时天气")
    String getWeather(String city) { return weatherApi.fetch(city); }
}
// AiServices.builder(Agent.class).tools(new WeatherTools()).build();

教学版 vs 生产版

维度 教学版(本场景) 生产协议(OpenAI/Claude/MCP)
工具声明 提示词里写清单 请求体 tools 数组 + JSON Schema
调用表达 模型输出 JSON 文本 响应 tool_calls 结构化字段
结果回填 user 消息拼接 role=tool 专用消息

形式不同,语义完全一致:先决策、再执行、后回填。读懂教学版再看原生协议,只是换了个"信封格式"。另有一个生产大坑:LangChain4j 默认对"模型幻觉出不存在的工具"抛异常断流,应改为返回可读错误或显式配置容错策略。

好的工具设计长什么样

  • 单一职责:一个工具只做一件事,复杂动作组合成技能(见后续场景);
  • 参数自描述:模型只见过 schema,参数名要"望文生义"(city 优于 c);
  • 失败要可读:返回"城市未收录"而不是抛堆栈,模型才能自我纠错。
graph TD
    A["模型输出的调用意图"] --> B{"工具在白名单?"}
    B -->|否| C["返回可读错误"]
    B -->|是| D{"有权限?"}
    D -->|否| C
    D -->|是| E["registry.invoke 执行"]
    E -->|成功| F["结果回填模型"]
    E -->|异常| G["翻译成模型能懂的失败文本"]
    G --> F

图示:execute() 执行层切面——白名单、权限、失败可读化三道关卡都在这里,模型永远触达不到底层资源。

面试考点

  • "模型能直接操作我的数据库吗?"——不能。模型只能输出调用意图(工具名 + 参数),所有副作用都发生在你的执行层,这是做权限控制和安全审计的天然切面。
  • "工具抛异常给模型会发生什么?"——异常信息若不可读,模型无法纠错,可能重试放大问题;应把失败翻译成模型能理解的文本回填。

生产化路径

  • 用原生协议替代提示词清单:LangChain4j @Tool / Spring AI @ToolCallback 自动生成 schema,避免手写漂移。
  • 执行层统一收口:白名单、参数校验、权限、审计日志、脱敏都做在 execute() 这一个切面上(后续沙箱、审批、Hooks 场景都在这层扩展)。
  • 工具命名空间化:多来源工具(内置 + MCP + 插件)用前缀区分,防止同名工具被模型误选。
FunctionCalling工具调用
https://clxhxhhr.top/posts/4704/
作者
clxstart
发布于
2026-10-13
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。