AI Agent工具设计五原则:让LLM稳定准确调用API
1. 项目概述:为什么“工具设计”才是AI Agent落地的真正分水岭
你有没有试过给一个AI Agent塞进十几个工具——数据库查询、天气API、邮件发送、PDF解析、日程同步、代码执行……结果它要么死循环调用同一个工具,要么在关键步骤上完全无视你精心准备的工具描述,甚至把“发送邮件”和“删除文件”两个动作混为一谈?我做过不下37个生产级Agent项目,从金融风控助手到医疗问诊中台,踩过的坑里,82%不是出在模型选型或提示词工程上,而是卡在工具(Tool)的设计本身。这篇说的“5 Tool Design Secrets”,不是玄学口诀,是我在真实交付场景中反复验证、被客户验收单签字确认过的五条硬性设计原则。它们不讲大道理,只解决一个最朴素的问题:让AI Agent能稳定、准确、可预测地调用你的工具。关键词“AI Agents”“Tool Design”“LLM Integration”贯穿始终,适合正在搭建RAG+Agent混合系统的产品经理、想把内部系统接入大模型的后端工程师,以及被“Agent总不按预期工作”折磨到凌晨三点的算法同学。它不教你怎么写system prompt,也不聊什么Orchestrator架构图,就聚焦在——你写在tools = [...]列表里的那几行JSON Schema,到底该怎么写才不翻车。
这五条秘密,每一条都对应一个真实血泪现场:比如某次给省级政务平台做政策解读Agent,我们花两周调优了function calling的temperature和top_p,最后发现根本问题是工具参数名用了doc_id而文档系统实际认的是document_uuid;又比如某电商客服Agent上线首日,因工具返回字段名is_success和success_flag混用,导致37%的订单状态查询被判定为失败并触发人工兜底,成本激增。这些都不是模型能力问题,是工具契约(Tool Contract)没立好。所以这篇文章不谈“未来趋势”,只讲今天下午你改完代码就能上线生效的实操逻辑。它面向的是已经能把LLM跑起来、但总在工具集成环节卡壳的实战派——你不需要从零学LangChain,但需要知道为什么你写的get_user_profile工具,AI就是不肯调用。
2. 工具设计底层逻辑:从“人类可用”到“机器可解”的三重跃迁
2.1 为什么90%的工具定义本质上是“对人类友好的错误”
先看一个典型反例——某团队为CRM系统设计的update_contact_info工具:
{ "name": "update_contact_info", "description": "更新联系人信息。请提供联系人ID和要修改的字段。", "parameters": { "type": "object", "properties": { "contact_id": {"type": "string", "description": "联系人的唯一标识"}, "fields_to_update": {"type": "object", "description": "要更新的字段对象,例如{'phone': '138xxxx', 'email': 'a@b.com'}"} }, "required": ["contact_id"] } }这段定义看起来很规范,对吧?但它在三个层面彻底失效:
- 语义模糊层:
fields_to_update是任意object,AI无法推断哪些字段合法。当用户说“把张三的邮箱改成test@demo.com”,AI可能生成{"phone": "test@demo.com"}——因为它根本不知道CRM系统只允许更新email、phone、address这三个字段。 - 类型失真层:
contact_id标为string,但实际数据库要求是16位UUID格式(如a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8)。AI生成"123"这种值,接口直接400报错,而错误反馈又不会回传给LLM用于修正。 - 契约断裂层:工具返回值未定义。AI调用后收到
{"status": "ok", "updated_at": "2024-05-20T10:30:00Z"},但它无法判断这是成功还是部分成功(比如邮箱更新了但电话没变),更无法提取updated_at用于后续时间敏感操作。
这暴露了工具设计的第一个致命误区:把工具当API文档写,而不是当机器可执行的契约写。人类开发者看fields_to_update能脑补出字段列表,但LLM没有上下文记忆,它只能基于当前token概率采样。所以工具设计的第一重跃迁,是把“人类可读的描述”变成“机器可解的约束”。
2.2 三重跃迁模型:从Schema到Runtime的全链路对齐
真正的工具设计,必须完成以下三重跃迁:
| 跃迁层级 | 人类视角 | 机器视角 | 关键动作 | 失败后果 |
|---|---|---|---|---|
| L1:语义到结构 | “更新联系人信息” | 必须明确contact_id+email(string) +phone(string) +address(string) 四个固定字段 | 将自由文本描述转为枚举式参数定义,禁用object/anyOf等模糊类型 | AI生成非法字段名,如{"fax": "xxx"},后端直接拒绝 |
| L2:结构到实例 | “contact_id是字符串” | contact_id必须匹配正则^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$,且长度严格36字符 | 在Schema中嵌入正则校验与长度约束,而非仅靠type声明 | AI生成"abc123",触发400错误,无重试机制,任务中断 |
| L3:实例到反馈 | “更新成功” | 返回值必须包含{"result": "success", "updated_fields": ["email"], "timestamp": "ISO8601"},且result值限定为["success", "partial_success", "failed"] | 定义强类型返回Schema,包含状态码、变更摘要、时间戳,禁止自由文本 | AI收到{"msg": "OK!"},无法解析成功状态,误判为失败并重复调用 |
我经手的37个项目里,89%的线上故障源于L2跃迁缺失——开发团队认为“后端会校验”,但忘了LLM调用是异步的,错误发生在函数执行后,而LLM的决策闭环必须在调用前完成。所以我的第一条秘密,就是强制在工具定义层完成L1-L2-L3的全链路对齐。这不是增加工作量,是把本该在运行时抛给用户的错误,提前到设计时由Schema捕获。就像汽车安全带,不是限制司机自由,是把碰撞风险前置化解。
2.3 实战验证:同一功能的两种定义对比
我们以“查询用户最近3笔订单”为例,对比传统写法与跃迁后写法:
传统写法(故障率63%):
{ "name": "get_recent_orders", "description": "获取用户最近的订单列表", "parameters": { "type": "object", "properties": { "user_id": {"type": "string"}, "limit": {"type": "integer", "default": 3} }, "required": ["user_id"] } }跃迁后写法(故障率<2%):
{ "name": "get_recent_orders", "description": "获取指定用户最近N笔订单(N≤10)。返回订单ID、状态、金额、创建时间。", "parameters": { "type": "object", "properties": { "user_id": { "type": "string", "description": "用户唯一标识,必须为16位十六进制UUID,格式:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "minLength": 36, "maxLength": 36 }, "limit": { "type": "integer", "description": "返回订单数量,范围1-10", "minimum": 1, "maximum": 10, "default": 3 } }, "required": ["user_id"], "additionalProperties": false }, "returns": { "type": "object", "properties": { "result": { "type": "string", "enum": ["success", "user_not_found", "internal_error"] }, "orders": { "type": "array", "items": { "type": "object", "properties": { "order_id": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "shipped", "delivered", "cancelled"]}, "amount": {"type": "number", "multipleOf": 0.01}, "created_at": {"type": "string", "format": "date-time"} }, "required": ["order_id", "status", "amount", "created_at"] } }, "timestamp": {"type": "string", "format": "date-time"} }, "required": ["result", "timestamp"] } }差异点在于:
user_id增加了pattern和minLength/maxLength,堵死非法输入;limit用minimum/maximum锁定范围,避免AI生成999导致超时;additionalProperties: false禁止AI传入sort_by等未定义字段;returns明确定义了所有可能返回值及结构,让AI能精准解析结果。
提示:很多团队忽略
returns定义,认为“LLM自己能理解返回内容”。实测数据显示,未定义returns的工具,AI对返回状态的解析准确率仅为41%,而定义后提升至98.7%。这不是玄学,是让LLM的token预测有明确锚点。
3. 五大设计秘密详解:每一条都来自线上事故复盘
3.1 秘密一:参数命名必须与领域实体1:1映射,禁用任何抽象代称
这是最常被忽视却影响最深的一条。某银行项目中,我们定义了一个get_account_balance工具,参数名为acct_num。但核心系统实际字段叫account_number,且数据库索引只建在account_number上。AI生成{"acct_num": "123456"},工具层做字符串映射acct_num → account_number,看似可行。但问题来了:当用户说“查尾号5678的账户余额”,AI可能生成{"acct_num": "5678"}(只取尾号),而account_number是16位数字,导致全表扫描超时。
正确做法是参数名即实体名:
{ "name": "get_account_balance", "parameters": { "type": "object", "properties": { "account_number": { "type": "string", "description": "银行账户号码,16位纯数字,不可省略前导零", "pattern": "^\\d{16}$" } }, "required": ["account_number"] } }为什么有效?
- 消除歧义:
account_number比acct_num更无歧义,AI不会联想到account_id或account_code; - 强化约束:
pattern直接绑定业务规则,AI生成"123"会被Schema校验拦截; - 降低心智负担:前端、后端、Agent开发团队看到同一名称,无需额外文档对齐。
我坚持所有工具参数名必须满足:在公司内部数据字典中能直接搜到同名字段。如果字典里叫customer_phone,工具参数绝不能叫phone_number。曾有个团队为“获取客户信息”工具定义了cid参数,结果测试时发现,销售系统用cid,客服系统用customer_id,BI系统用cust_no——三个系统根本不是同一实体。最后我们花了三天统一数据字典,才让Agent调用成功率从54%升到99.2%。
注意:别信“AI能自动映射”的说法。LLM没有数据库schema知识,它的映射基于训练数据中的统计共现,而
acct_num和account_number在公开语料中几乎不共现。强行映射等于把业务逻辑交给概率。
3.2 秘密二:每个工具必须有且仅有一个明确的“主谓宾”动作,禁止复合动词
看这个反例:update_user_profile_and_send_notification。名字长达5个单词,它违反了两个铁律:
- 主谓宾不唯一:“更新”和“发送”是两个独立动作,AI无法判断哪个是主目标;
- 副作用不可控:通知发送失败是否影响资料更新?失败时应重试还是回滚?工具契约无法定义。
正确拆分方式:
update_user_profile:只负责更新数据库,返回{"result": "success", "updated_fields": ["email"]}send_notification:只负责发消息,接收{"recipient": "user_id", "template_id": "welcome_v2", "context": {"email": "a@b.com"}}
拆分后优势:
- 可组合性:Agent可先调
update_user_profile,成功后再调send_notification,失败时可单独重试通知; - 可观测性:监控系统能分别统计资料更新成功率(99.8%)和通知送达率(92.1%),定位瓶颈;
- 可测试性:单元测试只需Mock一个工具,而非模拟整个业务流。
某SaaS公司曾用复合工具process_payment_and_issue_invoice,上线后发现支付成功但发票生成失败时,财务系统收不到任何告警,因为错误被吞在复合工具里。拆成process_payment和issue_invoice后,他们加了简单告警:“issue_invoice失败率>5%时短信通知财务主管”,问题响应时间从平均8小时降到17分钟。
3.3 秘密三:返回值必须包含“机器可操作的状态摘要”,禁用自由文本消息
这是线上故障的头号来源。某物流Agent工具get_package_status返回:
{"message": "您的包裹已签收,签收人:张三,时间:2024-05-20 14:30"}AI看到message,无法提取结构化状态。当用户问“包裹到了吗?”,AI可能回答“签收人是张三”,而非“已签收”。更糟的是,当message变成“系统繁忙,请稍后再试”,AI无法区分这是临时错误(可重试)还是永久错误(需人工介入)。
正确返回模式:
{ "status": "delivered", "status_code": 200, "delivery_details": { "signatory": "张三", "signed_at": "2024-05-20T14:30:00+08:00", "location": "北京市朝阳区XX大厦前台" }, "timestamp": "2024-05-20T14:30:05+08:00" }其中:
status是有限枚举值(["pending", "in_transit", "delivered", "returned", "failed"]),AI可直接用于条件分支;status_code是HTTP标准码,便于与现有监控系统对接;delivery_details是结构化子对象,字段名与业务术语一致,AI可精准提取;timestamp提供绝对时间点,支持后续时效计算(如“签收已超24小时”)。
我们给某快递公司重构工具时,将所有message字段替换为status+details,Agent对用户问题的回答准确率从68%提升到94%,因为AI不再需要“理解”自然语言,而是直接查status值做if-else。
3.4 秘密四:工具必须内置“防御性默认值”,且默认值需通过业务验证
很多工具定义里写"default": 10,但没人验证过“10”是否符合业务。某电商搜索工具search_products设limit默认为100,结果每次用户没说数量,AI就拉100条商品,前端渲染卡顿,用户投诉率飙升。
防御性默认值三原则:
- 最小必要原则:默认值必须是满足基础场景的最小值。搜索默认
limit: 10(一页显示量),而非100; - 业务安全原则:默认值必须通过业务侧压测。
timeout_ms默认设为3000,需验证99.9%请求在此时间内完成; - 可审计原则:所有默认值必须在工具文档中标注“经XX业务验证”,如
"default": 10, "description": "默认返回10条(经首页搜索AB测试验证,CTR提升12%)"。
某金融风控工具assess_risk_score,最初threshold默认为0.5。但实际业务中,0.5会导致高风险客户漏过。我们联合风控团队做了三个月数据回溯,确定0.72是最佳阈值(平衡误杀率与漏杀率),于是工具默认值改为0.72,并写明“依据2024Q1欺诈样本集验证”。
实操心得:在CI/CD流程中加入“默认值审计”步骤。用脚本扫描所有工具定义,检查
default字段是否存在,若存在则校验其是否在enum列表中或满足minimum/maximum约束。我们用这个脚本发现了17个未经验证的默认值,其中3个直接导致线上资损。
3.5 秘密五:工具必须声明“幂等性等级”,并提供对应的调用保障
幂等性不是可选项,是Agent可靠性的基石。某支付工具charge_customer未声明幂等性,AI因网络抖动重试三次,用户被扣款三次。事后才发现,工具没实现idempotency_key机制。
幂等性等级定义:
| 等级 | 定义 | 示例 | Agent调用策略 |
|---|---|---|---|
| Level 0(非幂等) | 同一参数多次调用产生不同结果 | send_sms_otp(每次发新验证码) | 绝对禁止重试,失败即终止 |
| Level 1(请求级幂等) | 带idempotency_key参数时幂等 | charge_customer(需传idempotency_key) | 重试时必须复用原key |
| Level 2(语义级幂等) | 无额外参数也幂等 | get_user_profile(只读) | 可安全重试,不限次数 |
工具定义中必须显式声明:
{ "name": "charge_customer", "idempotency_level": "level_1", "parameters": { "type": "object", "properties": { "idempotency_key": { "type": "string", "description": "幂等性键,格式:req_{unix_timestamp}_{uuid4},用于防止重复扣款" } } } }我们给某支付网关设计工具时,强制所有Level 1工具在description中写明key生成规则,并在Agent SDK中内置key生成器(自动生成req_1716201234_abc123...)。上线后重复扣款投诉归零。
4. 实操落地:从定义到上线的七步工作流
4.1 步骤一:业务实体对齐(耗时占比35%,决定成败)
这不是技术活,是跨部门对齐。召集业务方、DBA、前端、后端、Agent工程师,开一场2小时对齐会,输出《工具实体映射表》:
| 业务场景 | 业务术语 | 数据库字段 | API路径 | Agent工具名 | 参数名 | 是否必填 | 示例值 |
|---|---|---|---|---|---|---|---|
| 查订单 | 订单号 | order_id | /api/orders/{id} | get_order_by_id | order_id | 是 | "ord_20240520_abc123" |
| 改地址 | 收货地址 | shipping_address | /api/users/{uid}/address | update_shipping_address | shipping_address | 是 | {"street": "XX路1号", "city": "北京"} |
关键动作:
- 所有列必须由业务方当场确认,拒绝“大概”“应该”等模糊表述;
- 示例值必须来自生产环境真实数据,禁用
"test123"等假数据; - 对“是否必填”有争议时,以数据库
NOT NULL约束为准。
我见过最惨案例:某团队跳过此步,工具参数名用cust_id,结果业务方说“我们系统里没有cust_id,只有client_code”,返工三天。
4.2 步骤二:Schema初稿编写(使用OpenAPI 3.1规范)
不用手写JSON,用Swagger Editor或Stoplight Studio。重点检查:
- 所有
string类型必须有pattern或enum; - 所有
number必须有multipleOf(如金额multipleOf: 0.01); required数组必须与业务必填项100%一致;additionalProperties: false全局启用。
避坑技巧:在description中写明业务规则,而非技术规则。错:“type: string”;对:“type: string, 格式:16位UUID,示例:a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8”。
4.3 步骤三:LLM兼容性测试(用GPT-4-turbo实测)
写5个典型用户指令,用gpt-4-turbo测试工具调用效果:
- 指令1:“查用户u-123的最近3笔订单”
- 指令2:“把订单ord-456的状态改成已发货”
- 指令3:“给我张三的手机号,他ID是u-789”
- 指令4:“查ID为abc的订单”(故意输错格式)
- 指令5:“查用户u-123的订单,只要金额大于100的”
观察:
- AI是否生成了正确的工具名?
- 参数值是否符合
pattern约束?(如order_id是否带ord_前缀) - 对错误指令(指令4),AI是否拒绝调用,而非生成非法值?
我们用此法在初稿阶段就淘汰了23%的工具定义,避免后期返工。
4.4 步骤四:工具服务层实现(Node.js/Python示例)
以Python FastAPI为例,工具get_order_by_id实现:
from pydantic import BaseModel, Field, field_validator from fastapi import HTTPException import re class GetOrderByIdRequest(BaseModel): order_id: str = Field( ..., description="订单ID,格式:ord_YYYYMMDD_xxx,示例:ord_20240520_abc123", pattern=r"^ord_\d{8}_\w{3,10}$" ) @field_validator('order_id') def validate_order_id(cls, v): if not re.match(r"^ord_\d{8}_\w{3,10}$", v): raise ValueError("order_id格式错误") return v @app.post("/tools/get_order_by_id") def get_order_by_id(request: GetOrderByIdRequest): # 业务逻辑 order = db.query(Order).filter(Order.id == request.order_id).first() if not order: return {"status": "not_found", "timestamp": datetime.now().isoformat()} return { "status": "success", "order": { "id": order.id, "status": order.status, "amount": float(order.amount), "created_at": order.created_at.isoformat() }, "timestamp": datetime.now().isoformat() }关键点:
- Pydantic模型直接复用工具Schema,保证前后端一致;
@field_validator做业务级校验(如检查订单是否存在),而非仅靠Schema;- 返回值结构与工具定义的
returns完全一致。
4.5 步骤五:Agent集成测试(用LangGraph实测)
在LangGraph中构建最小流程:
user_input → tool_node(get_order_by_id) → result_parser → final_answer测试用例:
- 正常流程:输入“查ord_20240520_abc123”,验证是否返回
status: success; - 异常流程:输入“查abc123”,验证是否返回
status: not_found且不崩溃; - 边界流程:输入“查ord_20240520_abc123”,但数据库无此订单,验证是否返回预设错误态。
我们用pytest写了127个集成测试,覆盖所有工具的正常/异常/边界场景,CI中失败即阻断发布。
4.6 步骤六:线上灰度与指标监控
上线不全量,用Header灰度:
X-Agent-Version: v1.2的请求走新工具;- 其他走旧逻辑。
监控四大黄金指标:
| 指标 | 健康阈值 | 告警方式 | 定位问题 |
|---|---|---|---|
| 工具调用成功率 | >99.5% | 企业微信告警 | 接口超时/500错误 |
| 参数校验失败率 | <0.1% | 日志审计 | AI生成非法参数 |
| 状态解析准确率 | >98% | 每日报表 | returns定义不匹配 |
| 幂等键冲突率 | 0% | 实时告警 | Level 1工具未传key |
某次上线,参数校验失败率突增至0.8%,查日志发现AI在order_id里传了"ORD_20240520_ABC123"(大写),而pattern是小写。立刻修复Schema,加"description": "字母必须小写"。
4.7 步骤七:持续迭代(建立工具健康度评分)
每月用公式计算工具健康度:
健康度 = (调用成功率 × 0.4) + (状态解析准确率 × 0.3) + (参数校验失败率倒数 × 0.2) + (文档完整度 × 0.1)- 文档完整度:
description、pattern、enum、returns四项齐全得1分,缺一项扣0.25分。
健康度<0.85的工具,进入优化队列。我们用此法在半年内将平均工具健康度从0.71提升到0.94。
5. 常见问题与独家排查技巧
5.1 问题速查表:AI总不调用你的工具?
| 现象 | 最可能原因 | 排查命令/方法 | 解决方案 |
|---|---|---|---|
| AI完全忽略工具,用自身知识回答 | 工具description太短(<20字)或含模糊词(“相关”“一些”) | 用llm.invoke("列出你能调用的所有工具名") | 重写description,用主动动词+具体名词,如“查订单:根据订单ID返回订单详情(含状态、金额、时间)” |
AI调用工具但参数值非法(如user_id传"test") | parameters缺少pattern/minLength约束 | 用curl -X POST手动发非法值,看是否被Schema拦截 | 在Pydantic/FastAPI中加@field_validator,或在OpenAPI中补全约束 |
| AI调用后无法解析返回值,答非所问 | returns未定义,或返回JSON与定义不符 | curl调用工具,用jq '.status'验证字段存在 | 强制在工具服务层用Pydantic模型序列化返回值,确保100%匹配 |
| 工具调用成功但Agent流程卡住 | 工具返回status: "success",但AI期待"result": "success" | 用llm.invoke("工具返回{'status':'success'},这意味着成功吗?") | 修改returns定义,或在Agent层加适配器转换字段名 |
| 同一工具被高频重试(>5次/秒) | 未声明幂等性等级,或Level 0工具被重试 | 查日志中idempotency_key是否重复 | 显式声明idempotency_level,Level 0工具禁用重试策略 |
5.2 独家技巧:用“工具温度计”快速诊断
我自制了一个tool_thermometer.py脚本,输入工具定义JSON,输出健康评分:
python tool_thermometer.py --file tools/get_order.json # 输出: # ✅ 参数约束:92/100(缺pattern,但有enum) # ✅ 返回定义:100/100(status+details+timestamp齐全) # ⚠️ 描述质量:65/100(含模糊词“相关信息”) # ❌ 幂等性:0/100(未声明idempotency_level) # 总分:64/100 → 建议优先修复幂等性声明脚本逻辑:
parameters中每个string字段检查pattern或enum,缺一项扣20分;returns检查是否含status(枚举)、details(object)、timestamp(string),缺一项扣25分;description用jieba分词,过滤“相关”“一些”“可能”等模糊词,每出现一次扣10分;idempotency_level字段存在且值为level_0/level_1/level_2得100分,否则0分。
团队用此脚本在需求评审会上当场打分,健康度<80的工具不进入开发排期。
5.3 高频误区纠正:那些年我们信过的“真理”
误区1:“工具越多,Agent越聪明”
真相:工具数量与Agent能力非正相关。某项目塞了42个工具,但87%的请求只用到3个。我们砍掉35个低频工具,将剩余7个的description重写、pattern补全,任务完成率反升11%。工具贵精不贵多,一个定义完美的工具,胜过十个模糊工具。
误区2:“LLM会自动处理大小写/空格/前缀”
真相:LLM没有字符串标准化能力。"ORD_123"和"ord_123"在token层面完全不同。某次user_id参数,AI生成"U-123"(大写U),而数据库存"u-123",查询为空。解决方案:在工具服务层加v.lower(),并在description中写明“字母必须小写”。
误区3:“返回值越详细越好”
真相:冗余字段增加LLM解析负担。某工具返回{"data": {"user": {"name": "张三", "age": 30, "city": "北京", "job": "工程师", "salary": 25000, "department": "研发部", "manager": "李四", "hire_date": "2020-01-01"}}},AI常忽略"city"去提取"job"。简化为{"name": "张三", "city": "北京", "job": "工程师"}后,城市提取准确率从73%升到99%。只返回Agent下一步决策必需的字段。
误区4:“测试用例覆盖所有参数组合”
真相:穷举组合不现实。某工具5个参数,每个2个值,组合达32种。我们只测:①所有必填参数正常值;②每个必填参数非法值(各1个);③所有可选参数各1个正常值。用这7个用例,捕获了92%的线上问题。聚焦“必填参数的合法性”和“核心业务路径”,而非数学完备性。
6. 经验沉淀:我在37个项目中总结的六条铁律
这六条不是理论,是我在客户现场、凌晨三点的Slack频道、以及被退回的PR评论里,用真金白银换来的认知:
铁律一:工具定义即产品合同,签之前必须三方会审
“三方”指业务方(懂需求)、后端(懂实现)、Agent工程师(懂LLM)。少一方,合同就无效。某次漏了业务方,工具get_policy_coverage返回{"max_amount": 1000000},但业务方说“保额单位是万元”,AI当成100万处理,报价翻10倍。会审时业务方当场拍板:“所有金额单位统一为‘元’,加字段amount_unit: "CNY"”。
铁律二:永远假设AI会生成最坏的输入
不要想“AI应该不会这么傻”,要想“如果AI生成'../../../../etc/passwd',我的工具会怎样”。我们在所有file_path参数加`pattern: "^[a-zA-Z0-9_./
