应用系统体系架构-04:Skill 与 Tool Calling
Tool Calling / Function Calling 让模型应用能够获取实时信息、进行精确计算和改变业务状态。模型输出工具名称与参数,宿主程序检查并执行真实函数,再把结果送回模型。
本章中的 Skill 指具有明确输入输出的原子能力;任务规范文件中的 Skill 则描述如何组合和使用能力。函数接口、工作规范和编排流程需要分清,否则容易把一段描述误当成实际执行保障。
能力边界
纯模型推理不能直接知道当前数据库、文件或外部 API 状态,也不适合替代可靠的精确计算。可以让模型决定要查询什么,再让确定性工具提供结果。
例如计算字符串的 SHA-256,应执行真实算法:
1 | import hashlib |
同样,查询天气和修改空调温度是不同能力。前者取得信息,后者改变外部状态。副作用(Side Effect) 表示函数执行改变文件、数据库、设备或外部系统,不能只根据模型回复判断操作已完成。
SHA-256 是摘要算法,不是可解密的加密算法。输入字节、编码、大小写、空格和换行都会影响结果。模型生成一个看起来正确的 64 位 Hex 串,与程序实际计算出摘要,是不同的证据来源。
只在自己的应用中使用的能力,可以直接注册成模型 Tool,由宿主调用本地函数;需要跨宿主复用时,再通过 MCP 暴露同一业务接口。部署在本地还是远端不决定它必须属于哪一类,MCP 解决接入协议,Tool Calling 解决模型请求与宿主执行。
状态与纯函数
数学上的 只由当前输入决定。程序若让内部计数器参与返回值,调用顺序就会影响结果;但业务工具还可能读取当前数据库或时间,所以不能一概要求所有工具都是数学纯函数。
| 概念 | 实际含义 | 与其他概念的区别 |
|---|---|---|
| 纯函数 | 相同输入产生相同结果且没有可观察副作用 | 读取变化的外部数据通常不满足 |
| 无状态服务 | 不依赖实例内保存的用户会话状态 | 可以查询数据库或产生业务写入 |
| 无副作用 | 不改变外部业务状态 | 不一定保证结果恒定 |
| 幂等 | 重复相同逻辑操作的预期效果与一次一致 | 不要求响应字节或状态码相同 |
| 线程安全 | 并发访问仍满足约定不变量 | 可能需要锁、原子操作或事务 |
购物车需要按用户隔离,但不等于每个用户必须拥有一个独立 Service 实例。共享的 CartService 可以保持无会话状态,通过经过认证的用户标识读取数据库中的各自购物车。把“当前用户的购物车”放在共享服务的可变成员中,则可能让其他请求读取或覆盖它。
同样,函数里一个参与业务结果的共享 count 会制造历史依赖。监控计数可以单独维护并提供并发保护,不能把它与查询结果的业务计算混在一起。调用者身份应来自可信认证上下文,模型生成的 user_id 还需验证访问权。
调用协议
文本版 ReAct 往往从模型文字中提取 Action;原生 Tool Calling使用专门字段表达工具名、参数和调用 ID,减少自行解析文本的工作。
常见消息职责如下:
| 消息 | 作用 |
|---|---|
| system | 规定任务和行为边界 |
| user | 表达用户目标与补充信息 |
| assistant | 返回普通回答或工具调用请求 |
| tool | 返回某次调用的实际结果 |
模型提出调用时,应用必须保留这条 assistant 消息,再追加对应工具结果。返回结果需要与原调用 ID 一一对应,尤其不能在并发调用时通过返回顺序猜测。
1 | { |
执行后的消息:
1 | { |
不同接口可能使用 tool_calls / tool_call_id,或 tool_use / tool_result 等内容块。应用应在适配层转换成内部统一的调用对象,并保留原始标识。推理模型可能还有独立的推理内容块,执行器不应从内部推理文字猜测动作。
这条链路中有三种不同的结构化信息:
| 信息 | 用途 | 产生位置 |
|---|---|---|
| 工具定义与 Schema | 说明名字、参数及含义 | 应用或框架注册时 |
| 具体 Tool Call | 填入本次实际参数并给出调用 ID | 模型响应 |
| 执行结果 | 返回实际数据、错误与状态 | 受控执行器 |
用户说“取前三位 VIP 用户”时,limit=3 来自任务语义;Schema 中的 type: integer 则是参数定义,不能拿来当实际值。工具返回的内容再给模型组织最终回答,输出与执行事实仍需一致。
调用 ID 标识一次调用,不标识整个函数。read_file 读取 A.py、B.py、C.py 时,名称相同,但各有调用 ID 和路径;即使同样参数重试,也要正确区分请求与结果。调用 ID 也不自动成为防止重复业务写入的幂等键。
Schema 设计
工具定义包含名称、描述和参数 Schema。名称应表达领域与操作,描述说明用途、条件、单位和副作用,Schema 限定结构。
1 | { |
含糊的 handle(data) 难以说明适用条件;bookstore_check_inventory(isbn) 更容易理解。多个服务的 search、get_log 等名称还需稳定的领域或来源前缀,避免工具碰撞。
description 与 docstring 可以进入模型看到的契约,但不能自动执行业务规则。模型即使生成合法参数,宿主仍需核对权限、目标存在性和允许状态。
命名应同时表达动作、目标和边界。例如 get_user 没有说明按 ID、邮箱还是其他属性查找,get_active_user_by_email 则说明只查询激活用户并使用邮箱定位。复杂领域可再加稳定的服务前缀。
参数描述还应区分单位与授权语义:金额是元还是分,日期使用哪一时区,include_roles 是否只是返回已允许查看的角色信息。让模型传入一个布尔值,不能直接赋予它读取敏感授权信息的权限。
受限解码
Constrained Decoding 在生成时约束合法 Token。系统依据当前 JSON 或文法状态屏蔽非法候选,再对允许候选重新归一化。设 为当前状态允许的 Token 集合,可表达为:
这种机制改善格式遵循,但无法判断库存数量是否真实、SQL 是否获授权或路径是否安全。Schema 支持范围、输出截断、拒绝响应和服务错误仍需处理。
模型先产生词表上的实数 logits,它们不是已经归一化的概率。对当前文法状态 不允许的候选,可以把对应值置为负无穷:
因为 ,非法候选不会被选中,合法候选重新归一化。BNF/GBNF 等文法描述结构与当前可接受位置,并不是一个对整段文字固定生效的字符黑名单。JSON 字符串中的 < 等字符可能是合法数据,不能一律删除。
格式约束发生在支持它的解码实现中。仅在 Prompt 里写“请遵守语法”不等于已经启用该机制,执行器仍需校验实际参数。
生成约束负责形式,执行校验负责业务边界。 二者需要共同使用,不能互相替代。
类型契约
Pydantic 可以表达字段类型、枚举、嵌套对象和集合,并导出 JSON Schema。下面以受控补丁请求说明复杂参数设计。
1 | from enum import Enum |
Enum 限定操作集合,嵌套对象表达每个文件的修改,列表长度限制一次规模,extra="forbid" 拒绝未定义字段。可选字段是否可以省略,取决于默认值与契约定义;Optional 仅表示允许空值时,不一定自动表示可缺省。
field_validator 在收到已有输入之后执行。所谓生成前校验,只能检查用户输入或要发送的 Schema,不能提前验证模型尚未产生的参数。跨字段约束可以使用模型级验证器,实际路径、对象版本和权限则要在执行前再次确认。
能限定值域时,不应只声明宽泛字符串。例如 base_branch 只允许 main/develop,可以使用 Literal 或 Enum;但允许值需要来自实际项目约定,不能把这两个分支名当成所有仓库的固定规则。
格式与内容检查也要分开。一个传统五字段 cron 表达式可先检查字段数,但 99 * * * * 仍有非法分钟;不同调度器还可能使用六或七字段。数量检查只是初筛,还需范围、符号、方言和时区验证。
自动注册
装饰器可以收集函数签名、类型与说明,并建立执行注册表。这样普通函数与对外 Schema 保持关联,减少重复维护。
1 | import inspect |
这个注册器仅支持明确参数,不尝试自动解释任意 *args、**kwargs 或无类型输入。成熟框架还需要处理别名、字段描述、版本和复杂类型。自动导出之后,应检查默认值、必填项、枚举及嵌套约束是否符合预期。
Python 装饰器可以近似理解为定义后执行 function = decorator(function)。它可以返回包装函数,也可以只读取签名、登记 Schema 后返回原函数;上面的注册器就是后一种方式。无需改写核心计算,便能增加发现和验证信息。
Java 注解则主要表达标记与元数据,具体由框架、运行时反射或编译期处理器实现。类似 requires_approval 的自定义声明,只有执行器真正识别并拦截才有效,不能认为写到注解中就必然弹出批准界面。
执行适配
宿主从模型结果取得调用后,按注册表解析与校验,再执行函数。实际执行器还应加入授权、超时、审计和结果验证。
1 | import json |
未知系统异常不应静默伪装成业务成功,完整错误可保存在受控日志中,模型只收到必要诊断。真实服务也不能仅凭函数名调用任意对象,而应只允许明确注册且获授权的能力。
HTTP 封装器还需要检查响应状态码、处理连接失败、读取模型调用结构和保存原始消息。调用结果回写之后,模型才能继续推理或生成最终回答。
并发与发现
独立查询可以并发,存在依赖或写冲突的操作需要顺序执行。理想情况下,多个工具等待时间由 接近降低为 ,实际仍包含模型请求、网络和调度开销。
查询库存与竞争对手价格可以并发;先创建工单再使用工单 ID 上传附件必须顺序执行。并发结果通过调用 ID 对应,并限制同时运行的任务数。
大量工具全部注入会消耗上下文并增加选择难度。可以先按领域、任务或向量检索筛选候选,再加载详细 Schema。这个过程称为 按需工具发现,需要分别评价候选召回与最终执行成功率。
检索库里存的可以是工具和 Skill 描述,不一定是业务文档:先把名称、用途和领域建立索引,查询到有限候选,再将详细契约给模型选择。具体宿主是否采用这套机制需要看实现,不能仅因安装了很多 Skill 就假定全部都会自动经过 RAG。
并发调用还需检查共享数据、状态和版本。三个文件的读取结果可能以不同顺序完成,按调用 ID 关联即可;若同时修改同一对象,就需要事务或并发控制,无状态接口本身也不能自动消除外部资源冲突。
原子能力
原子工具应具有清晰职责和有限输入输出。无状态接口不依赖隐含会话,但仍可能修改外部系统,因而不能把“无状态”解释为“无副作用”。原子能力表示职责粒度,不自动提供数据库事务的原子性。
图形计算器可以说明如何拆分:GUI 负责显示和事件,输入流维护表达式及退格,Parser 根据括号和优先级生成 AST,计算引擎执行求值。把这些都放进一个类也可能运行,但界面变化会影响计算实现,测试和复用就更困难。
Skill 与 Tool 也应按内聚职责分解:界面或任务入口负责取得意图,参数模型表达契约,执行器调用业务能力,验证器检查结果。拆分不是单纯限制代码行数,而是让不同变化原因有清楚边界。
副作用分类
| 类型 | 例子 | 控制重点 |
|---|---|---|
| 只读 | 查库存、读文件、检索文档 | 数据范围和隐私 |
| 可修改 | 更新记录、应用补丁 | 授权、范围、版本与恢复 |
| 对外操作 | 发信、发布内容、提交订单 | 明确批准、幂等与审计 |
| 破坏性 | 删除数据库记录、清理资源 | 目标核对、预演及审批 |
工具元数据可以描述只读、破坏性和幂等特征,但宿主不能只相信标签。需要把说明与实际实现核对,再按风险分配权限和执行方式。
“会修改状态”也不必然意味着非幂等,例如把库存设置为明确值,与在库存上增加一个数,是不同语义。只读提示、幂等提示和需要审批是不同维度,不能用一个标签代替全部风险判断。
数据库工具
数据库查询和修改应分成不同契约。查询工具限制允许表、查询类型、行数和超时;修改工具明确操作对象、预期版本、影响范围和 dry_run。
1 | query_database: |
SQL 字符串前缀或简单正则不能完整判断安全性。值应参数化绑定,表名和列名需要白名单,数据库账号本身还应具有有限权限。批量更新必须核对实际影响行数,避免一个条件错误修改全部记录。
可以把 生成查询 与 执行查询 拆成两步:模型形成候选,程序检查类型、权限、语句结构和参数绑定,再由受限执行器查询。这样可以独立审核候选,不把推理与实际写入藏在同一函数里。
复合索引列顺序仍取决于查询、选择性、排序和数据库计划。过滤 status=PAID 与 user_id 的例子,不意味着所有查询都应永久把 status 放在第一列;应以实际执行计划验证。
隔离执行
零信任执行 意味着模型生成的参数、代码和外部观察都需核对。沙箱隔离从轻到重可以分为进程、容器和 MicroVM,但每层都需要实际配置。
| 隔离方式 | 作用范围 | 主要限制 |
|---|---|---|
| 受限进程 | 用户、目录、环境和资源 | 普通子进程不会自动隔离 |
| 容器 | 文件系统、网络与资源配置 | 通常仍共享宿主内核 |
| MicroVM | 更强的虚拟化边界 | 启动、资源和运维成本更高 |
代码解释器可以基于 Docker 或专门沙箱服务实现。需要限制挂载、网络、运行用户、CPU、内存、进程数、时间和输出,避免默认继承宿主凭证。
Shell 工具宜使用固定可执行文件和参数数组,避免把自由生成的字符串直接交给 shell=True。黑名单难以覆盖管道、展开、编码和别名,因此应以能力白名单与隔离环境为主要约束。
独立进程主要分开地址空间,仍可能拥有同一用户的文件、网络和凭证权限。容器通常用 namespace 隔离所见资源、用 cgroup 控制配额,再配合运行用户、Capability 与挂载限制;并不是为每个容器另建一个物理启动区。MicroVM 则具有不同的虚拟化与内核边界。
自研书店助手需要自己实现这些 Host 侧检查。IDE 或聊天客户端提供的批准框、命令限制与执行器,不会因为换成一个直接调用模型的后端接口就自动存在。
超时与清理
超时必须同时处理等待和真实执行。取消协程不一定终止线程、子进程或远端任务;杀死 Docker CLI 也不等于容器已经退出。
下面的示例突出独立容器和最终清理。它用于说明执行流程,实际服务还需要身份、工作目录和流式输出限制。
1 | import subprocess |
这里在结束后截断返回文本,不能限制捕获阶段的全部内存占用。处理不可信大量输出时,应流式读取并在预算内终止或截断。清理失败也需要记录和后续回收,不能把失败容器留在后台。
长任务可以返回任务 ID,并持久化状态后异步完成;调用方可以查询或恢复任务,避免一个 HTTP 连接持续等待人工处理。
审批与回滚
Human-in-the-Loop(HITL) 把高风险动作挂起,向有权限的人展示具体操作。审批需要绑定任务、目标、参数和资源版本;恢复时再次核对,不能只看到“批准”文字就执行。
dry_run 生成预计变化,实际执行还可能因资源变化产生不同结果。数据库事务可回滚本地写入,Git 可以恢复本次文件差异,外部 API 则可能需要补偿动作。邮件已经发出时,数据库回滚不会将其撤回。
回滚应只覆盖当前任务的变化,避免覆盖用户已有修改或并发任务。隔离工作区、版本检查和明确事务边界可以减少恢复冲突。
审批若允许修改参数,应对修改后的动作重新校验,并绑定最终目标和版本。预演差分描述预计变化,快照记录可恢复的基线;保存一个对话 Context 并不能自动恢复数据库或文件。执行失败后,还需识别已发生的真实变化,选择受控回滚或补偿。
凭证与观察
密钥、数据库密码和个人信息应由宿主管理,模型通常只需要业务结果。工具可以使用内部配置或凭证句柄,避免把 AK/SK 写进参数、日志或返回内容。
HTTPS/TLS 保护通信链路,但模型服务作为接收方仍能看到发送的应用内容。因此,给传输加密不等于可以把真实数据库密码放入 Prompt。证书主要用于相应身份和密钥体系,也不能简单等同于“把用户名密码加密成一个证书”。
更直接的结构是让工具在宿主内部取得凭证,模型只看到受限业务参数或必要句柄。句柄解析还需绑定当前身份与允许服务;真实解密、密钥和连接过程都留在受控执行层。
Observation 清洗包括限制体积、脱敏、来源标识和内容隔离。外部日志或文档中的恶意指令不能变成系统规则,正则过滤也无法证明所有注入已被消除。
只返回最后若干字符可能丢失根因。测试日志应保留失败名称、异常类型、关键堆栈和退出码,完整日志存到受控位置,后续需要时再读取相关片段。
清洗返回结果也不能仅删去 <、system 等字符串:它们可能是合法源码或需要审查的文本,恶意指令又可以换一种表达。重点是保持资料来源与权限边界,限制它引发的下一步动作,而不是把过滤结果当成已证明安全。
空查询结果也未必是逻辑错误,例如库存为零或没有匹配图书可以是正常业务状态;容器资源不足、参数结构错误和业务不存在应分别反馈。
异常反馈
工具错误可以分成三层:契约错误、运行时错误、逻辑错误。参数类型不符属于契约错误,网络或依赖失败属于运行时错误,执行完成却违反业务断言属于逻辑错误。
1 | { |
结构化反馈帮助控制器选择补参数、重新读取、有限退避、降级或停止。Self-Correction 利用反馈调整下一轮动作,但不能保证第二轮必然成功。
重试需要检查失败是否可恢复、请求是否幂等以及观察是否变化。权限拒绝、确定性业务失败和相同无变化错误不应无限重试。完整堆栈可保存在服务侧,模型只收到必要信息。
日志摘录
长日志可以采用“先提取相关错误,再保留首尾”的预算兜底策略。一个示例是保留开头 20 行与末尾 50 行,标出中间省略量;尾部通常包含最新结果,但根因也可能在中部。
1 | def excerpt_log(text: str, head: int = 20, tail: int = 50) -> str: |
行数并不等于 Token 数,单行也可能非常长。实际还需字符或 Token 上限,并优先保留失败测试、异常类型、关键堆栈与退出码。只有首尾片段不足以定位时,应按完整日志标识再次取出相关部分。
能力评测
Pass@1 衡量一次尝试是否完成任务,工具选择的 Precision、Recall 和 F1 衡量是否选中合适能力。参数合法率与执行成功率还需要单独统计。
评测应覆盖正常任务、缺失参数、未知工具、权限拒绝、超时、业务冲突和错误恢复,并记录调用次数、成本及延迟。最终有一段自然语言回答不等于工具任务成功。
Precision 与 Accuracy 需要分清。前者是“选中的正例里有多少真的为正”,后者是“全部样本里有多少判断正确”。假设 100 张图片中有 10 张猫、90 张狗,以猫为正例:
| 策略 | Precision | Recall | Accuracy |
|---|---|---|---|
| 全部判为猫 | 10% | 100% | 10% |
| 全部判为狗 | 无正例预测,需约定处理 | 0% | 90% |
| 选中 9 张猫与 1 张狗 | 90% | 90% | 98% |
只看 Accuracy 会让类别不均衡掩盖漏召回,只看 Recall 又可能鼓励把全部候选都交给模型。工具选择也应同时检查误选和遗漏。调整阈值会影响权衡,但更好的表示或模型也可能同时改善两项,不能把它们理解为必然此消彼长。
常见反模式包括工具过多、名称模糊、参数无类型、职责混杂、缺少结果校验、隐式副作用和无界循环。改进应落到明确契约、能力筛选、执行控制和验证器上。
审计案例
自动 SQL 审计与代码 Patch 可以拆为读取源码、识别风险、生成补丁、应用差异、运行沙箱测试和提交待审结果。读操作与写操作应分别注册。
控制器在修改前保存任务范围,在应用后检查测试和业务目标。补丁没有报错并不表示问题已经修复;测试通过后,还要确认没有扩大接口或引入新风险。
这条链路把协议、契约、隔离、审批、回滚、异常和评测连接起来。各能力保持有限职责,上层负责协调状态与终止条件。
工具的可理解性来自清楚的名称和契约,可靠性来自受控执行与结果验证。模型选择能力和程序执行能力应始终保持明确边界。

