温嘉琪 / BUILDING SOMETHING FUN

Agent 的能力边界由 Server 决定

给 Manitodo 加 MCP Server 的过程里,遇到过一个很小但很能说明问题的决定。

create_goal 这个工具,title 字段要求过去完成时——用户说「我想学日语」,Agent 写进去的应该是「掌握了日语 N2」。这个转换逻辑不是自然而然的,Agent 不会凭空知道。你不写在 tool description 里,它大概率直接把用户的原话填进去。

这让我意识到 MCP Server 的设计重心在哪里:不是跑起来,是替 Agent 提前想清楚它不知道自己不知道的事。


设计 list_goals 的时候,第一个问题是返回多少字段。全字段太重,光 title 又不够——Agent 看到「学习日语」这四个字,不知道这个 Goal 具体要达到什么,大概率还要再调一次 get_goal 才能做任何判断,多一次 round trip。

最后选了 hybrid:title + selected_vision + counts。selected_vision 是用户当初设定的那句方向描述,有了它,Agent 通常能直接判断「这条进度该归到哪个 Goal」,不需要再查一次。

get_goal 的 progress notes 也有类似的取舍。tasks 要全返回,Agent 需要完整结构才能判断「这个 Goal 还有什么没做」。但 progress notes 是时序数据,用了几个月之后 payload 会很大——Agent 其实只需要最近的上下文,返回最近 5-7 条就够了。


response 里加 next_step 字段是另一个关键决定。

空状态是最需要引导的时机。Agent 拿到空的 goal 列表,如果没有任何提示,它只能回去问用户「你想做什么」,对话在这里断掉了。next_step 直接给「用 create_goal 创建一个」,Agent 可以接着走。

这个字段要做成 contextual 的,不是静态文案。create_goal 之后 task 列表为空,next_step 就建议 create_taskget_goal 返回的 tasks 大部分已完成,建议记录进度或者加新任务。Server 知道刚刚发生了什么,基于这些事实生成建议,不需要推断用户意图,逻辑很直接。

错误响应也是对称的设计。goal not found 之后建议先调 list_goals 看有哪些可用——Agent 自己推断不出来这个,它不知道你有哪些工具、工具之间的依赖关系,但 Server 完全知道。


tool description 的写法也有一个原则逐渐清晰:读者是 Agent,不是用户,也不是开发者。

不能用触发词写法——「当用户说’我做了 X’时调用这个工具」太脆,用户说「今天搞定了登录模块」和「修完了那个 bug」是同一个意图,不应该用字面匹配来描述。也不能写成技术文档——「POST 一条 progress_note 记录到数据库」,这是给开发者看的,对 Agent 的调用决策没有帮助。

应该是 Agent perspective:这个工具能做什么,什么情况下应该调它,配两三个具体场景。Agent 做的是模式匹配,例子比定义有效。

最值得投入的是 create_goal,因为它是唯一一个有语义转换要求的工具。其他工具语义直接,用户说做了什么,Agent 记什么,两个例子就够。精力放在最容易出错的地方。


整个设计过程里反复出现同一个问题:这个信息,Agent 自己能推断出来吗?

能推断的,不要放进 Server 的逻辑里。推断不出来的——工具之间的依赖关系、title 的语义格式、空状态之后该做什么——必须在 Server 层显式说清楚。Agent 的自主性建立在这些细节上,不是建立在它有多聪明上。