三个月前,我们的客服AI干了一件"正确"的蠢事。客户说"把坏掉的商品退了",它听懂了"退",然后选了cancel工具。结果一笔240美元的部分退款,变成了2400美元的全额账务冲销。客户暴怒,CFO黑脸。
问题很诡异。cancel在英语里确实可以表示"撤销、反转",AI的语义理解没毛病。但cancel工具在我们的系统里,执行的是会计账簿层面的完整交易冲销,跟客户想要的退款完全是两码事。
![]()
我花了三周时间折腾提示词工程。加约束、给示例、强调边界条件,各种技巧轮番上阵。测试集上的工具调用准确率,死死卡在71%。
![]()
最后解法简单到离谱:改名字。
我过去命名MCP工具的方式,跟写RESTful API一个思路——找个动词,找个名词,拼起来,干净。比如cancel、refund。这种命名对人类程序员很友好,但对大语言模型是灾难。
关键认知差在这里:API端点有明确的边界上下文,调用者知道自己处在哪个业务域。但LLM看到的是全局工具列表,它只能靠语义相似度做选择。"Cancel"和"reverse"在向量空间里太近,"refund"和"partial refund"的区分度又太弱。
这个陷阱早有理论预警。Eric Evans 2003年在《领域驱动设计》里提出"通用语言"(Ubiquitous Language),强调同一领域内的术语必须精确一致。今年Russell Miles把这套框架搬到AI Agent设计,Dennis Traub在Dev.to的文章标题更直白——"你的Agent一直在用那个词"。
核心规则:工具名要携带边界上下文,而不是描述操作本身。
我们的改造分三步。
第一步,前缀锁定领域。customer_support_cancel_order、accounting_reverse_transaction,一眼就能看出工具属于客服域还是会计域。
第二步,描述里互相指涉。每个工具的docstring明确说"这不是什么",并指向该用的替代工具。customer_support_cancel_order的注释强调:"停止履约。不发退款。不动会计账。"accounting_reverse_transaction则写明:"完整冲销已记账交易。非客户发起的退款请用customer_support_refund_partial。"
第三步,返回类型也带上下文。Pydantic模型命名跟工具前缀保持一致,让模型在调用链里始终处在同一语义场。
改造前后的代码对比很说明问题。
之前:
@mcp_tool
def cancel(order_id: str) -> dict:
"""Cancel an order."""
@mcp_tool
def refund(order_id: str, amount: Decimal) -> dict:
"""Refund a customer's payment."""
之后:
![]()
@mcp_tool
def customer_support_cancel_order(order_id: str) -> CustomerOrderCancellation:
"""Cancel a customer's order before it ships.
Stops fulfillment. Does NOT issue a refund. Does NOT touch accounting."""
@mcp_tool
def customer_support_refund_partial(
order_id: str,
amount: Decimal,
reason: RefundReason,
) -> CustomerRefund:
"""Issue a partial refund on a shipped order.
For full refunds use customer_support_refund_full."""
@mcp_tool
def accounting_reverse_transaction(
transaction_id: str,
reason: ReversalReason,
) -> AccountingReversal:
"""Reverse a posted accounting transaction. Full reversal only.
NOT for customer-initiated refunds. Use customer_support_refund_partial."""
500例held-out测试集跑完,准确率从71%跳到94%。没有改模型,没有加训练数据,没有动提示词模板。
技术实现层面,我们用FastAPI+MCP框架。核心结构是把领域模型和工具注册解耦,让命名规范能强制执行。
from mcp.server.fastmcp import FastMCP
from fastapi import FastAPI
from pydantic import BaseModel
from decimal import Decimal
mcp = FastMCP("customer-support-server")
class CustomerRefund(BaseModel):
refund_id: str
order_id: str
amount: Decimal
reason: RefundReason
audit_log_id: str
@mcp.tool()
def customer_support_refund_partial(
order_id: str,
amount: Decimal,
reason: RefundReason,
) -> CustomerRefund:
...
这个案例暴露了一个深层误区:我们把LLM当成能理解系统架构的工程师,实际上它只是个在语义空间里做近邻搜索的模式匹配器。你的工具命名越像给人类程序员看的,它越容易选错。
领域驱动设计的老方法,在AI时代意外复活。不是因为它时髦,而是因为LLM的"无知"恰好需要这种显式的边界标记。当你把cancel拆成customer_support_cancel_order和accounting_reverse_transaction,你强迫模型在做选择前先定位自己在哪个业务上下文里。
71%到94%的差距,本质是"隐式上下文"和"显式上下文"的差距。提示词工程试图让模型自己推断边界,命名工程直接把边界写在模型眼前。后者更笨,但更可靠。
这个经验对正在搭建AI Agent的团队有个直接启示:别急着优化模型,先检查你的工具命名是不是还在用2010年代的API思维。语义精确度比语法正确性重要十倍——尤其是当错误成本以千美元计时。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.