把 Agent 从概念做成可靠系统

导语
上一篇,我们走完了一次 Tool Calling:应用提供工具定义,模型返回 Tool Call,Runtime 校验并执行,再把结果送回模型。
其中有一个环节被刻意略过了:模型怎样知道该选哪个工具,参数又该怎么填?
假设一个开发 Agent 同时拥有两个工具:
search_code
search_logs
用户说:
帮我找出 INVALID_SIGNATURE 是从哪里产生的。
它应该搜代码,还是搜运行日志?如果工具描述都只写“搜索内容”,模型只能猜。
Tool Schema 就是模型看到的操作说明书。它用工具名、用途描述和参数约束,回答三件事:
- 这个工具是做什么的;
- 什么情况下应该用它;
- 调用时需要提供哪些结构化参数。
Schema 设计不好,强模型也可能选错工具、漏传参数或扩大查询范围。本文会用一组“代码搜索与日志搜索”工具,完整演示怎样把模糊能力改造成清晰接口。
一、先看一个看似能用的坏 Schema
const searchTool = {
name: 'search',
description: '搜索信息',
parameters: {
type: 'object',
properties: {
input: { type: 'string' }
}
}
}
这份定义没有语法错误,却几乎没有帮助模型做决策。
模型不知道:
- 搜索的是源码、日志、文档还是互联网;
input是关键词、正则表达式还是一段自然语言;- 搜索范围在哪里;
- 参数是不是必填;
- 结果为空时意味着什么。
更麻烦的是,如果工具实现允许 input 同时表达动作和参数,它就会成为一个万能入口:
search({ input: "在生产日志里找错误并顺便删除重复记录" })
Runtime 很难对这种自由文本做可靠校验和细粒度授权。
一个能通过 JSON Schema 校验的定义,不一定是一个适合模型使用的 Tool Schema。
二、工具名应该表达一个清晰动作
工具名是模型做选择时最先看到的信号之一。
相比:
github
data_tool
handle
execute
下面这些名字更容易形成稳定边界:
get_pull_request
search_application_logs
read_file
create_review_draft
实用的命名习惯是“动词 + 对象”:
get:获取一个已知对象;list:列出一组对象;search:根据条件查找未知位置;create:创建新资源;update:修改已有资源;delete:删除资源。
这不是为了追求英文整齐,而是让动作的副作用和返回预期更明显。
例如 get_order 与 update_order 应该分开。前者可以自动读取,后者可能需要确认。如果合成 order_tool,权限系统还要再次解析参数才能判断风险。
名字不要承担全部解释
不要为了写清边界,造出过长的名字:
search_staging_application_logs_by_exact_error_code_only
工具名负责识别,完整条件交给 description 和参数 schema。三者要协作,而不是把所有信息压进名字。
三、Description 要写选择边界,不是宣传语
下面这种描述信息量很低:
强大、智能、快速地搜索日志。
模型真正需要知道的是:什么时候用、能查什么、不能做什么。
例如:
查询指定环境中的应用运行日志。
适用于根据时间范围、服务名、请求 ID 或错误码定位运行时问题。
不用于搜索源码;搜索源码请使用 search_code。
该工具只读,不会修改日志或服务状态。
这段描述提供了四类信号:
- 能力:查询应用日志;
- 触发场景:定位运行时问题;
- 反边界:不搜索源码;
- 副作用:只读。
当两个工具容易混淆时,互相写清“不要在什么情况下使用”很有效。但不必给每个工具堆十条反例;只有真实存在选择冲突时才添加。
四、参数名要让模型知道“该填什么”
继续改造日志工具:
const searchApplicationLogs = {
name: 'search_application_logs',
description: [
'查询指定环境中的应用运行日志。',
'用于根据服务、时间范围、请求 ID 或错误码定位运行时问题。',
'不用于搜索源码;搜索源码请使用 search_code。',
'该工具只读。'
].join(' '),
parameters: {
type: 'object',
properties: {
environment: {
type: 'string',
enum: ['staging', 'production'],
description: '要查询的部署环境'
},
service: {
type: 'string',
description: '服务名,例如 auth-api'
},
query: {
type: 'string',
minLength: 1,
description: '日志查询表达式,可包含错误码或请求 ID'
},
startTime: {
type: 'string',
format: 'date-time',
description: '查询起始时间,ISO 8601 格式'
},
endTime: {
type: 'string',
format: 'date-time',
description: '查询结束时间,ISO 8601 格式'
},
limit: {
type: 'integer',
minimum: 1,
maximum: 200,
default: 50,
description: '最多返回多少条日志'
}
},
required: [
'environment',
'service',
'query',
'startTime',
'endTime'
],
additionalProperties: false
}
}
这里每个字段只表达一个概念。
不要把几个概念塞进一个字符串:
{
"filter": "staging auth-api last 30 minutes limit 50"
}
这种格式看起来省字段,实际上把解析工作重新推给模型或工具实现。拆成结构化参数后,Runtime 才能检查时间范围、环境和最大返回条数。
五、required、默认值和 null 不是一回事
JSON Schema 中,在 properties 里声明字段,并不自动代表它必填。必填字段要放入 required。
{
"properties": {
"service": { "type": "string" }
},
"required": ["service"]
}
还要区分三种状态:
字段缺失
字段存在但值为 null
字段存在且使用默认语义
如果 schema 只允许 string,传入 null 并不等同于省略字段。
默认值也不要只写在描述里。更稳妥的做法是:
- schema 用
default告诉读者和工具系统推荐值; - Runtime 或工具实现真正补齐默认值;
- 日志记录补齐后的最终参数。
不同校验器不一定会自动应用 default,不能假设“写了 default 就一定改写输入”。
六、Enum、范围和格式是可执行边界
能枚举的值,不要让模型自由拼写:
{
"environment": {
"type": "string",
"enum": ["staging", "production"]
}
}
相比任意字符串,它可以拦截:
prod
online
正式环境
production-eu-secret
数值也应该有合理范围:
{
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 200
}
}
否则模型可能请求返回十万条日志,既慢又占满上下文。
格式约束能表达日期时间、URI 等常见结构,但要确认你使用的校验器是否实际启用了对应 format 检查。Schema 是契约,Runtime 使用的验证行为才是最终事实。
七、为什么建议关闭额外字段
JSON Schema 默认允许未声明的额外属性。也就是说,只写 properties 时,这类输入可能仍会通过:
{
"service": "auth-api",
"query": "INVALID_SIGNATURE",
"deleteAfterRead": true
}
对于边界明确的工具,通常可以设置:
{
"additionalProperties": false
}
这样模型多传字段时,Runtime 会明确拒绝,而不是静默忽略或把未知字段传给下游。
不过复杂 schema 使用 allOf 等组合关键字时,additionalProperties 的作用域容易产生意外。不要机械添加后就结束;要用真实样例验证合法和非法输入。
八、一个工具应该做多大一件事
工具太大,模型难选择,权限也难控制:
github({ action, payload })
工具太碎,模型又需要在几十个近似动作中犹豫:
get_issue_title
get_issue_body
get_issue_author
get_issue_labels
更合适的边界通常对应一个可理解、可授权、可测试的业务动作:
get_issue
list_issue_comments
create_issue_comment
update_issue_labels
判断是否应该拆分,可以问四个问题:
- 不同动作的副作用是否不同?
- 是否需要不同权限或确认策略?
- 参数和错误类型是否明显不同?
- 模型是否经常只需要其中一部分能力?
任意两项差异很大时,拆开通常更清晰。
但这不是绝对公式。最终要用真实任务集测试,而不是只凭接口美感判断。
九、不要让模型填写系统已经知道的信息
假设用户已在产品里打开仓库 acme/web-app,服务端也知道当前账号和组织。
这时工具参数未必需要再次暴露:
{
"userId": "?",
"tenantId": "?",
"accessToken": "?",
"owner": "?",
"repo": "?"
}
可信信息应从执行上下文注入:
async function execute(
args: SearchCodeArgs,
context: ToolContext
) {
return codeSearch.search({
tenantId: context.tenantId,
repository: context.currentRepository,
query: args.query
})
}
这样既减少模型出错,也避免它把一次会话里的资源标识带到另一位用户或另一个租户。
原则是:模型只填写完成语义动作所需、且确实需要它判断的参数;身份、凭证和已确定的资源上下文由应用提供。
十、Schema 需要怎样测试
Schema 不是写完看着合理就结束。至少要准备三组测试。
选择测试
给模型一组真实用户请求,检查它是否选对工具:
“找出这段函数在哪里被调用” → search_code
“查 request_id=abc 的线上错误” → search_application_logs
还要加入容易混淆的反例。
参数测试
检查正常、边界和非法参数:
缺少必填字段
时间范围颠倒
limit 超过上限
多出未知字段
environment 使用未允许值
JSON Schema 负责结构约束;像“startTime 必须早于 endTime”这样的跨字段业务规则,通常仍需要额外代码验证。
权限测试
同一份合法参数,在不同用户和环境下可能得到不同结果:
开发者查询 staging → 允许
开发者查询 production → 需要额外权限
外部用户查询内部服务 → 拒绝
Schema 不能替代授权测试。
十一、前端和后端如何共同使用 Schema
后端把 Schema 用于模型提示、参数校验和工具路由。前端也可以从同一份元数据中获得帮助:
- 展示即将执行的工具名称;
- 把关键参数转成确认摘要;
- 为人工修正参数生成表单;
- 在提交前进行基础校验。
但不要直接把原始 Schema 无脑渲染给用户。query 对模型可能很清楚,对普通用户却需要显示成“日志查询条件”。产品层可以维护安全、可本地化的展示元数据。
同一份底层契约可以服务多端,但模型描述、开发者文档和用户界面不必使用完全相同的文案。
十二、用一组工具检查是否理解
现在设计一个订单查询 Agent,已有三个工具:
get_order
list_fulfillment_events
cancel_order
试着回答:
- 为什么不能合成一个
order_tool? cancel_order的描述应该怎样明确副作用?- 订单所属租户应该由模型传入吗?
reason是必填、可空还是可省略?依据是什么?- 哪些非法参数可以由 JSON Schema 拒绝,哪些仍需业务代码检查?
如果你能根据权限、动作边界和业务语义回答,而不只是堆更多字段,就已经掌握了 Schema 设计的核心。
十三、这一篇真正要记住的事
Tool Schema 不只是“把 TypeScript 类型翻译成 JSON”。它同时服务三个目标:
- 帮模型在相似能力中选对工具;
- 把自然语言意图约束成可校验参数;
- 给 Runtime 留下权限、测试和审计的清晰边界。
设计时优先检查:
名字是否表达明确动作
描述是否说明使用场景与反边界
字段是否一项一义
必填、枚举、范围和额外字段是否受控
可信身份是否来自服务端上下文
读写动作是否因权限和副作用而拆分
Schema 解决的是“模型怎样提出一份清楚的调用请求”。下一篇继续看另一半:工具执行完后,怎样把结果返回给模型,才不会让它误判、浪费上下文或陷入重试。
下一篇预告
下一篇:Tool Result 设计:Agent 如何看懂工具返回?
参考资料
- OpenAI API — Function calling:
https://developers.openai.com/api/docs/guides/function-calling - JSON Schema — Object:
https://json-schema.org/understanding-json-schema/reference/object - Model Context Protocol — Tools:
https://modelcontextprotocol.io/specification/2025-06-18/server/tools
192

被折叠的 条评论
为什么被折叠?



