面向 LLM 的程序设计 7:工具描述的工程化——name、description、parameters 怎么写才少误用

张开发
2026/4/10 7:23:13 15 分钟阅读

分享文章

面向 LLM 的程序设计 7:工具描述的工程化——name、description、parameters 怎么写才少误用
当你的系统里工具一多订单、售后、知识库、工单、权限审批……Agent 误选工具往往不是因为“模型不够聪明”而是因为工具元数据本身不可区分name不稳定、description不写边界、parameters既不严格也不对齐后端。结果就是模型把“查某个订单”当成“按用户列订单”把“创建退款申请”当成“审批退款”甚至在不知道关键 ID 的情况下硬调用一个需要order_id的工具并编造参数本篇为系列第七篇第六篇从生命周期角度把「定义→注入→决策→校验→执行→回注」串起来本篇聚焦其中最容易被轻视、却最决定成败的起点——工具定义的工程化写法。你会看到写好工具描述不是“堆更多字”而是像写一份“给另一个工程师看的接口契约”把可发现性、可区分性、可约束性做出来。摘要工具元数据应做到“三稳定”name稳定唯一、description稳定表达用途与边界、parameters稳定且可校验JSON Schema并与后端校验同源。有效的description必须包含「做什么、何时用、何时不用、与相邻工具区别、输入前置条件、输出形状」。parameters侧通过枚举、pattern、additionalProperties、oneOf 等降低“自由填参”空间。配套 demo 用同一组电商售后工具构造“差的工具描述”和“工程化工具描述”两套 registry在同一批真实用户句上做对比评估展示误选率与参数合法率的差异。关键词Tool metadata工具描述JSON Schema工具误选参数约束工具可发现性代码链接面向 LLM 的程序设计 7工具描述的工程化案例代码1 先把问题说透工具误用的 3 种常见形态想象一下你把一套内部后台开放给新人同事如果按钮都叫“操作”“处理”“查询”新人再聪明也会点错。模型也是一样——它主要依赖你给的工具说明书来选路。1.1 误选选错工具实际例子用户说“帮我看看订单ORD-9123为什么还没发货”模型却调用list_orders_by_user因为它看到 description 都写“查询订单”没有边界差异。理解要点工具之间的“语义距离”太近而元数据无法拉开距离时模型会把它们当成同类按钮随机试。1.2 乱填参数乱编、结构不合法实际例子工具需要refund_id形如RFD-前缀模型却填了一个“看起来像”的字符串或者把amount填成99.0字符串导致编排器或服务端校验失败。理解要点如果parameters没有给出 enum / pattern / required / additionalProperties 的硬约束模型就会把参数当作文案。1.3 越权调用了不该调用的写/敏感工具实际例子用户只是在咨询“能不能退款”模型直接调用approve_refund审批而不是preview_refund预演/解释。理解要点你需要在工具元数据里写出“何时不用”与“前置条件”并配合后续的最小权限/两阶段确认系列安全篇。2 工具定义的“工程化四件套”把工具元数据当成一份可被 LLM 读懂且可被程序校验的契约你至少要把下面四件事写清楚。2.1name稳定、唯一、能当“键”稳定一旦对外使用name就是协议的一部分。避免今天叫refund_create明天叫create_refund_request_v2但没有版本策略参见第 4 篇 API 版本化。唯一不要出现多个工具 name 只差一个后缀且无语义search/search2。可当键编排器会用它路由到真实实现HTTP/RPC/本地函数你也要用它做指标工具调用成功率、误用率。理解要点name是“按钮 ID”不是“按钮文案”。按钮文案写在description里。2.2description不是“介绍”而是“边界与差异”推荐把description写成一个小模板可复制到所有工具里每段都短但信息密度高做什么What一句话说清输出是什么不是“处理订单”而是“根据订单号返回订单详情与当前物流状态”。何时用When触发条件“当你已掌握明确order_id时”。何时不用When NOT明确禁止的误用路径“不要用它去按用户枚举订单不要在缺少 order_id 时猜测”。与相邻工具区别Contrast至少点名 1 个相邻工具说明差异“与list_orders_by_user的差异本工具只接受order_id”。输入前置条件Prereq是否必须先通过某工具拿到 id“需要先通过search_orders或用户提供订单号”。输出形状Shape给模型一个“字段锚点”“返回response_typeorder_detail含status、buyer_user_id”。实际例子差 vs 好差查询订单好当已掌握明确 order_id 时查询单笔订单详情返回 order_detailstatus、buyer_user_id、logistics_state。不要用于按用户枚举订单与 list_orders_by_user 不同本工具不接受 user_id。2.3parameters用 Schema 把“可填空间”收紧你不需要一上来就用最复杂的 JSON Schema但至少做到required缺了必填就早失败在执行前拦下见第 6 篇生命周期第 4 阶段。enum状态、渠道、币种等用枚举不要让模型自由造词。pattern对 ID 形状做约束^ORD-[0-9]{6,}$、^RFD-[A-Z0-9]{8}$显著降低“编造 id”。additionalProperties: false避免模型偷偷塞入未声明字段这类“多余字段”会污染下游。理解要点Schema 的价值不是“让解析器开心”而是让模型在填参时有更强的约束锚点。实际例子差 vs 好以“创建退款申请”工具为例差的例子Schema 太“松”容易乱填/编造{name:create_refund_request,description:退款处理。,parameters:{type:object,properties:{order_id:{type:string},reason:{type:string},amount:{type:number}}}}缺少required模型可能漏传order_id或reason还继续调用。缺少enumreason变成自由文本后端很难做稳定分支与统计。缺少patternorder_id形状不受约束模型更容易编造/写错格式。缺少additionalProperties: false模型可能夹带未声明字段例如user_id、approvetrue污染下游与审计。好的例子Schema 收紧“可填空间”更可控{name:create_refund_request,description:为指定订单创建退款申请写操作。仅当用户明确要申请退款且已给出合法订单号时使用。,parameters:{type:object,properties:{order_id:{type:string,pattern:^ORD-[0-9]{6,}$,description:订单号例如 ORD-700001},reason:{type:string,enum:[damaged,wrong_item,no_longer_needed,late_delivery,other],description:退款原因分类必须从枚举中选},evidence_note:{type:string,maxLength:300,description:证据补充说明可选}},required:[order_id,reason],additionalProperties:false}}required把“缺关键字段”变成执行前可拦截的确定性失败。enum把自由文本变成“选项题”显著降低乱填与口径漂移。pattern把 ID 从“看起来像”变成“必须长得对”降低编造与格式错误。additionalProperties: false禁止夹带字段避免把不受控信息带入后续链路。2.4 返回体让模型“读懂结果”也要工程化工具返回如果只是一句话会导致下一步推理缺少事实锚点模型容易“编造细节”。建议在 tool 结果中至少带okbooleanresponse_type判别字段关键 id 与关键状态字段_links若下一步可导航呼应第 5 篇超媒体实际例子差 vs 好以get_order_by_id的 tool 结果为例差的例子只有一句话缺少事实锚点{ok:true,message:订单已经发货了}缺少response_type难以稳定区分“订单详情/列表/错误”等多种返回形态。缺少关键字段没有order_id、status、buyer_user_id、物流状态等模型后续更容易“脑补”运单号/时间/链接。缺少_links模型无法可靠发现下一步可执行的操作只能猜接口或硬编码路径。好的例子结构化 判别字段 关键事实 可导航{ok:true,response_type:order_detail,order_id:ORD-912345,status:shipped,buyer_user_id:user_1024,logistics_state:in_transit,_links:{self:{href:https://api.example.com/v1/orders/ORD-912345,method:GET},list_by_user:{href:https://api.example.com/v1/users/user_1024/orders?statusshippedlimit10,method:GET},refund_preview:{href:https://api.example.com/v1/refunds/preview?order_idORD-912345,method:GET}}}response_type给模型一个稳定分支锚点减少误读字段。关键字段把事实钉住避免幻觉补全尤其是“看似合理但不存在”的字段。_links把“允许的下一步”显式写出来让 Agent 能自导航而不是乱猜。3 案例电商售后助手里最容易混淆的 8 个工具本篇 demo 用一个更贴近生产的工具簇同属“订单/售后/工单”域刻意制造“相邻工具语义接近”的情况get_order_by_id单笔订单详情list_orders_by_user用户维度订单列表search_orders按关键词/手机号后四位/时间范围检索订单可能返回多个候选create_refund_request创建退款申请写preview_refund_policy解释是否可退、需要哪些材料读approve_refund审批退款敏感写search_kb_articles搜知识库读create_support_ticket创建工单写3.1 你最需要防的不是“调用失败”而是“调用成功但做错事”实际例子用户说“这个订单能退吗”如果模型直接approve_refund从 HTTP 角度可能 200 成功但业务上是越权误操作。理解要点工程化描述的目的不是让工具“更常被调用”而是让它只在正确语境下被调用。4 通过案例对比“差描述 vs 工程化描述”配套 demo 做三件事同一套工具实现同名、同参数构造两份 registryregistry_bad.py同域多工具共用完全相同的 description订单三条一段、退款三条一段、知识与工单一段刻意糊掉边界仅靠函数名仍可能选对时再用「口语、多线索、read/write 边界」的用例拉开差距。registry_good.py按本篇模板写清边界与差异用同一批评测用户句含依赖关系与敏感操作让模型只输出“应调用的工具名 arguments”或声明不调用。汇总指标工具选择准确率、参数 Schema 通过率、敏感工具误触发率。4.1 实例代码逐步讲解每一步做什么、为什么、会看到什么结果下面按 demo 的真实执行顺序进行描述。第 0 步准备运行环境让 demo 能连上模型demo 需要能调用一个支持 tool calling 的 Chat API.env里至少配置API_KEY以及可选的BASE_URL、MODELmain.py会通过config_parser.load_config()读取这些配置然后在_build_llm()里构造ChatOpenAI(...)温度固定为0为了减少随机性理解要点你要对比的是“描述写法”导致的差异所以把温度降到 0能让结果更可复现但不同模型/版本仍可能有差异。第 1 步准备评测数据集EVAL_CASES评测用例定义在eval_cases.py的EVAL_CASES13 条刻意减少与工具名「撞脸」的用语并增加歧义句。每条用例包含case_id用例编号user_text用户原话expected_tool_name这句话“应该选择”的工具名或None表示不应调用任何工具notes为什么这么判帮助你理解期望完整列表见仓库内eval_cases.py。刻意加入的典型张力包括c09_order_id_over_user_scope同句既有买家编号又有单号时应走单笔详情、c10/c11「只要政策」vs「要正式建单」、c12/c13不说「知识库/工单」时的排障 vs 开单。第 2 步构造两套工具元数据bad vs good这一步是实验的核心两套 registry 的工具名与参数 Schema 完全同源只改description的写法。registry_common.build_tool_schemas()定义所有工具的 JSON Schema含required/enum/pattern/additionalProperties:falseregistry_bad.build_bad_registry()同域工具共用同一段模糊 description源码里order_generic/refund_generic/support_generic例如订单侧三条工具都挂同一段文字模型仅凭 description无法区分「单笔 / 列表 / 搜索」边界。registry_good.build_good_registry()按本篇模板写清边界、差异、前置条件与禁用路径get_order_by_id:{name:get_order_by_id,description:(做什么当已掌握明确 order_id 时查询单笔订单详情返回 order_detailstatus、buyer_user_id、logistics_state。\n何时用用户给出完整订单号ORD-...并询问该订单状态/详情/物流。\n何时不用不要用于按用户枚举订单不要在缺少 order_id 时猜测或编造。\n区别与 list_orders_by_user 不同本工具只接受 order_id不接受 user_id。与 search_orders 不同本工具不做模糊检索。),parameters:schemas[get_order_by_id],},list_orders_by_user:{name:list_orders_by_user,description:(做什么按 user_id 列出该用户近 N 天订单列表可按 status 过滤返回 order_listorders[], total。\n何时用用户明确给出 user_id并希望“列出/汇总/筛选”多笔订单。\n何时不用不要用于查询单笔订单详情如果没有 user_id先用 search_orders 或向用户追问。\n区别与 get_order_by_id 不同本工具不接受 order_id。),parameters:schemas[list_orders_by_user],},理解要点因为 Schema 同源所以结果差异主要来自description对“选工具”的影响而不是参数约束的差异。第 3 步把 registry 转成模型可见的 tools 列表注入main.py的_run_suite()会先把 registry 转成 OpenAI 风格的tools数组tools as_openai_tools(registry)见registry_common.as_openai_tools然后每条用例构造两条消息SystemMessage(content_system_prompt())HumanMessage(contentc.user_text)并调用ai llm.bind_tools(tools).invoke(msgs)你可以把这一步理解为第 6 篇生命周期中的阶段 2注入把 tools 让模型“看得见”阶段 3决策模型在本轮是否产出 tool call第 4 步读取模型的 tool_calls只取第一个作为“选择结果”模型返回的AIMessage里可能有tool_calls。demo 为了让评测足够“干净”做了一个简化_pick_first_tool_call(ai)只取第一个 tool call若模型并行调用多个这里先忽略返回值chosen工具名例如search_ordersargs参数 dict例如{keyword:7788,limit:5}raw_calls原始tool_calls保留用于排查理解要点这个 demo 的目标不是编排多步工作流而是隔离变量、只比较“工具定义写法影响选型”。生产编排中你会更完整地处理多 tool_calls并行/依赖/排序见第 6 篇。第 5 步处理“明确不应调用任何工具”的分支no_tool_system_prompt()明确要求不该调用时仅输出no_tool。main.py会检查如果模型content no_tool且没有tool_calls则记录chosenNoneschema_okNone因为没参数可校验sensitive_misfireFalse这一步对用例c06_sensitive_approval_should_not非常关键正确行为应该是 no_tool而不是调用approve_refund。第 6 步对 arguments 做 JSON Schema 校验schema_ok如果chosen存在且args是 dictdemo 会用 registry 中同名工具的parameters做校验Draft202012Validator(schema).validate(args)并记录schema_okTrue表示“即使真的要执行这组参数在执行前校验也能过”schema_okFalse表示“模型虽然选了工具但填参不合法”常见于漏必填、enum 不在集合、pattern 不匹配理解要点这里校验的是编排器侧的早失败能力第 6 篇阶段 4它与真实服务端校验是两层防线。第 7 步统计“敏感工具误触发”sensitive_misfiredemo 把approve_refund标为敏感工具SENSITIVE_TOOLS {approve_refund}并定义一个“敏感误触发”的判定如果某用例expected_tool_name is None不应调用任何工具但模型实际chosen in SENSITIVE_TOOLS则sensitive_misfireTrue最终会累计到sensitive_misfires理解要点这类指标在生产里非常有用——它能把“工具调用成功但做错事”的风险量化出来。第 8 步生成汇总指标你会看到的输出 1/2_summarize(results)会输出三类汇总tool_choice_accuracy工具选择准确率expected chosen的比例schema_ok_rate参数合法率在“确实调用了工具”的样本里arguments 通过 schema 的比例sensitive_misfires敏感工具误触发次数终端输出中你会看到两段汇总 1) 差描述bad registry汇总 2) 工程化描述good registry汇总 它们的差异就是这篇文章要强调的核心只改描述写法工具选型质量通常就能明显改善尤其是相邻工具很多时。第 9 步输出逐条对照表你会看到的输出 3最后main.py会输出一个数组逐条用例对照每条包含case_idexpectedbad_chosen/bad_schema_okgood_chosen/good_schema_ok如何解读如果bad_chosen ! expected但good_chosen expected通常说明工程化description把边界差异“讲清楚”了。如果schema_ok在 good 下更高往往是因为 good 的 description 更强调前置条件与必填字段模型更愿意按 schema 填参。如果 bad 出现approve_refund而 good 变成no_tool说明 good 的“何时不用/敏感约束”有效降低越权冲动。4.2 典型结果长什么样示意不同模型输出会有差异但形状基本一致。你通常会看到类似{total_cases:13,tool_choice_accuracy:0.5,schema_ok_rate:0.8,sensitive_misfires:1}以及逐条对照数组。如下是运行后的结果。这个结果还不能充分说明问题但是这篇文章对于 Tools 的四点说明确实对于落地大模型产品非常重要 工具描述工程化对比评测第 7 篇 cases13 modelqwen-flash 1) 差描述bad registry汇总 { total_cases: 13, tool_choice_accuracy: 0.9230769230769231, schema_ok_rate: 1.0, sensitive_misfires: 0 } 2) 工程化描述good registry汇总 { total_cases: 13, tool_choice_accuracy: 1.0, schema_ok_rate: 1.0, sensitive_misfires: 0 } 3) 逐条对照只展示 tool name 与 schema [ { case_id: c01_order_detail, expected: get_order_by_id, bad_chosen: get_order_by_id, bad_schema_ok: true, good_chosen: get_order_by_id, good_schema_ok: true }, { case_id: c02_need_search_then_detail, expected: search_orders, bad_chosen: search_orders, bad_schema_ok: true, good_chosen: search_orders, good_schema_ok: true }, { case_id: c03_list_user_orders, expected: list_orders_by_user, bad_chosen: list_orders_by_user, bad_schema_ok: true, good_chosen: list_orders_by_user, good_schema_ok: true },

更多文章