
1. 从用户反馈石沉大海说起为什么我要给智能体加一个建议提交工具做智能体开发的人大概都有过这种体验用户用着用着突然在对话框里敲一句你们这个功能能不能改改或者要是能支持XX就好了。这些反馈往往就散落在聊天记录里运营同学要一条条翻产品同学要手动整理最后真正能进入迭代流程的十不存一。我在维护一个问答类智能体的过程中这个问题尤其突出——用户提建议的意愿其实很强但缺少一个顺手就能提交的通道。这次要聊的就是给一个叫看山的智能体加一个MCP工具专门用来接收用户提交的改进建议。MCP是Model Context Protocol的缩写简单理解就是一套让智能体能够调用外部能力的标准接口协议。你可以把它想象成给智能体装了一个工具箱每个工具负责一件事智能体在对话过程中判断什么时候该用哪个工具。加了这个建议提交工具之后用户只要在对话里表达我想提个建议智能体就能自动识别意图、收集内容、调用工具把建议结构化地存下来而不是让用户跳转到某个表单页面去填一堆字段。这篇文章适合三类人看一是正在做智能体应用、想给智能体扩展外部能力的开发者二是对MCP协议感兴趣、想找一个完整落地案例来练手的技术同学三是产品侧的同学想了解怎么把用户反馈链路做得更顺滑。我会从工具的设计动机讲起把MCP工具的注册、参数定义、调用逻辑、异常处理、实测踩坑都掰开揉碎讲一遍。代码部分我会用Python来演示因为目前MCP的Python SDK生态相对成熟社区示例也多照着改就能跑。需要提前说明的是MCP工具的开发核心不在于代码有多复杂而在于意图识别和参数设计这两件事。工具本身可能就几十行代码但怎么让智能体在合适的时机调用它、怎么设计参数让用户不用填表就能提交、怎么处理用户中途反悔或者信息不全的情况这些才是真正花时间的地方。下面我按实际开发的顺序一步步展开。2. MCP工具到底是什么先搞清楚智能体调用外部能力的底层逻辑2.1 用点菜来理解MCP的工作方式很多人第一次接触MCP会有点懵觉得又是一个新概念。其实你可以把它类比成餐厅点菜。智能体是服务员用户是顾客MCP工具就是厨房里的一道道菜。顾客说我想吃辣的服务员不会自己去炒菜而是把需求翻译成厨房能听懂的指令——宫保鸡丁一份。厨房做好之后服务员再把结果端回来。在这个类比里MCP协议规定了服务员怎么把需求传给厨房以及厨房怎么把结果传回来的标准格式。工具的定义就相当于菜单每道菜有名字、有说明、有配料表参数。智能体看到菜单之后会根据顾客的话来判断该点哪道菜。如果顾客说我想提个建议智能体就知道该调用提交建议这道菜然后把顾客说的内容作为配料传进去。这个机制的关键在于智能体本身不需要知道建议存到哪里、用什么格式存它只需要知道有这么个工具调用它需要传什么参数。具体的存储逻辑、数据校验、后续处理全部封装在工具内部。这种解耦设计让智能体的能力可以无限扩展——今天加一个提交建议的工具明天加一个查询订单的工具智能体的核心逻辑不用动。2.2 MCP工具和普通函数调用的区别在哪有同学可能会问这不就是函数调用吗我自己写个函数让智能体调不就行了区别在于标准化和可发现性。普通函数调用需要你在智能体的代码里硬编码什么时候调哪个函数而MCP工具是通过协议注册的智能体在运行时能动态发现有哪些工具可用、每个工具需要什么参数。这就好比一个是写死的if-else一个是插件化的架构。具体到我们这个场景MCP工具的优势体现在三个方面。第一工具的描述信息名称、功能说明、参数说明会作为上下文提供给智能体智能体根据这些描述来判断是否调用不需要我在代码里写一堆意图判断的逻辑。第二参数的校验和类型转换由协议层处理我只需要声明这个参数是字符串、那个参数是枚举剩下的交给框架。第三工具可以独立部署和更新改建议提交的逻辑不需要重新发布整个智能体。2.3 一个MCP工具的最小构成要素一个能跑起来的MCP工具至少包含这几个部分工具名称智能体用来识别和调用的标识、工具描述告诉智能体这个工具是干什么的什么时候该用、参数定义每个参数的名字、类型、是否必填、说明、执行函数真正干活的代码。这四样东西缺一不可尤其是工具描述和参数说明写得清楚不清楚直接决定了智能体能不能在正确的时机调用它。我见过不少开发者工具写得没问题但描述写得太简略结果智能体要么该调的时候不调要么不该调的时候乱调。比如工具描述只写提交建议智能体可能理解成用户说任何话都算建议。所以描述里一定要写清楚触发条件比如当用户明确表达想要提出改进意见、功能建议或反馈问题时使用。3. 动手之前先想清楚建议提交工具的参数该怎么设计3.1 从用户的一句话里能提取出哪些结构化信息用户提建议的时候说的话往往很随意。可能是你们这个搜索能不能支持模糊匹配啊也可能是我觉得界面颜色太刺眼了建议改改还可能是提个建议希望增加夜间模式。如果直接把这些原文存下来后续分析的时候还得靠人工读一遍。所以工具的参数设计目标就是尽量把非结构化的自然语言转成结构化的字段。我最终确定的参数有五个建议内容必填用户原话或智能体整理后的描述、建议类型选填枚举值比如功能建议、体验问题、内容纠错、其他、紧急程度选填枚举值比如高、中、低、用户标识选填用来关联提交者方便后续跟进、补充联系方式选填用户愿意留就留不愿意留不强求。这五个参数里只有第一个是必填的其余都是选填目的是降低提交门槛——用户说一句话就能提交不用被一堆必填项拦住。这里有个设计取舍值得说一下。我一开始想把建议类型也设成必填因为分类对后续处理很有帮助。但实测下来发现用户经常说不清楚自己提的是哪类建议智能体强行追问反而让对话变得很啰嗦。后来改成选填智能体根据内容自动判断类型判断不准就填其他用户完全无感。这个改动让提交完成率明显提升。3.2 参数类型和校验规则的具体设定参数定义不是随便写写就行类型和校验规则直接影响到工具调用的稳定性。下面是我实际使用的参数定义表用表格列出来更清楚参数名类型是否必填说明校验规则contentstring是建议的具体内容长度1-2000字符去除首尾空白后不能为空categorystring否建议分类枚举值功能建议、体验问题、内容纠错、其他默认其他prioritystring否紧急程度枚举值高、中、低默认中user_idstring否用户标识长度不超过64字符仅允许字母数字和连字符contactstring否联系方式长度不超过128字符不做格式强校验校验规则这块我踩过一个坑。最开始content字段我没设长度上限结果有用户把一整篇小作文贴进来几千字存进去之后后续处理很麻烦。后来加了2000字符的上限超出的部分智能体会提示用户精简一下。另外user_id的字符限制也是必要的防止有人传特殊字符导致存储层出问题。3.3 为什么参数说明比参数本身更重要参数定义只是骨架参数说明才是让智能体正确填参的关键。每个参数的description字段我都会写清楚这个参数填什么、什么情况下填、不填会怎样。比如category的说明我写的是根据建议内容判断分类。如果用户明确说了是功能需求就填功能建议如果是抱怨现有功能不好用就填体验问题如果是指出内容有错误就填内容纠错判断不了就填其他。这种写法看起来啰嗦但实测效果很好。智能体在填参的时候会参考这些说明分类准确率比只写建议分类四个字高出一大截。同理priority的说明我会写用户明确表达着急、影响使用就填高一般性建议填中锦上添花的想法填低。这些判断逻辑写在参数说明里比写在智能体的系统提示词里更合适因为它是跟工具强绑定的。4. 代码落地从零写一个能跑的建议提交MCP工具4.1 环境准备和依赖安装动手之前先把环境搭好。我用的是Python 3.10以上版本MCP的Python SDK通过pip安装即可。除了SDK本身还需要一个存储层来保存建议数据演示阶段我用SQLite就够了生产环境可以换成MySQL或者直接调内部API。pip install mcp如果你用的是其他语言的SDK安装方式类似核心概念是通的。装完之后建一个项目目录结构大概是这样的一个主文件用来定义和注册工具一个存储模块负责数据落库一个配置文件放数据库连接等参数。目录结构不用太复杂工具本身逻辑不重过度设计反而增加维护成本。提示MCP SDK的版本更新比较快建议锁定一个稳定版本再开发避免中途升级导致接口不兼容。我用的版本在开发期间就改过一次参数定义的写法锁版本能省不少事。4.2 工具注册与参数声明的完整代码下面是我实际使用的工具注册代码做了适当简化去掉了业务相关的敏感信息。核心结构就是声明工具名称、描述、参数schema然后绑定执行函数。from mcp.server import Server from mcp.types import Tool, TextContent import json server Server(kanshan-suggestion-tool) server.list_tools() async def list_tools(): return [ Tool( namesubmit_improvement_suggestion, description( 当用户明确表达想要提出改进建议、功能需求、体验反馈或内容纠错时调用此工具。 用户只是闲聊或提问时不要调用。 ), inputSchema{ type: object, properties: { content: { type: string, description: 建议的具体内容保留用户原话或整理后的描述1-2000字符 }, category: { type: string, enum: [功能建议, 体验问题, 内容纠错, 其他], description: 根据建议内容判断分类判断不了填其他 }, priority: { type: string, enum: [高, 中, 低], description: 用户明确表达着急填高一般建议填中锦上添花填低 }, user_id: { type: string, description: 当前用户的标识用于关联提交者 }, contact: { type: string, description: 用户愿意留下的联系方式不愿意留则不填 } }, required: [content] } ) ]这段代码里最关键的是description字段的写法。工具描述我特意加了用户只是闲聊或提问时不要调用这句因为实测发现不加这句的话智能体有时候会把用户的普通提问也当成建议提交。参数描述里也埋了判断逻辑让智能体填参时有据可依。4.3 执行函数校验、落库、返回结果工具被调用之后执行函数负责真正的处理。这里要做三件事参数校验、数据落库、返回结果给智能体。参数校验不能完全依赖协议层因为协议层只做类型检查业务规则还得自己写。server.call_tool() async def call_tool(name: str, arguments: dict): if name ! submit_improvement_suggestion: raise ValueError(f未知工具: {name}) content arguments.get(content, ).strip() if not content: return [TextContent(typetext, text建议内容不能为空请补充具体内容。)] if len(content) 2000: return [TextContent(typetext, text建议内容过长请精简到2000字以内。)] category arguments.get(category, 其他) priority arguments.get(priority, 中) user_id arguments.get(user_id, ) contact arguments.get(contact, ) try: suggestion_id save_suggestion( contentcontent, categorycategory, prioritypriority, user_iduser_id, contactcontact ) return [TextContent( typetext, textf建议已提交编号{suggestion_id}。感谢你的反馈我们会认真评估。 )] except Exception as e: return [TextContent( typetext, text提交时遇到问题请稍后重试。 )]返回结果这块有个细节要注意给智能体的返回文本会直接影响它怎么回复用户。我一开始返回的是提交成功结果智能体就干巴巴地回一句提交成功。后来改成带编号和感谢语的版本智能体的回复就自然多了。所以返回文本要当成给智能体的提示来写而不是简单的状态码。4.4 存储层的简单实现存储层我用SQLite演示建一张表存建议数据字段跟参数对应再加一个自增主键和提交时间。import sqlite3 from datetime import datetime def init_db(): conn sqlite3.connect(suggestions.db) conn.execute( CREATE TABLE IF NOT EXISTS suggestions ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, category TEXT DEFAULT 其他, priority TEXT DEFAULT 中, user_id TEXT DEFAULT , contact TEXT DEFAULT , created_at TEXT NOT NULL ) ) conn.commit() conn.close() def save_suggestion(content, category, priority, user_id, contact): conn sqlite3.connect(suggestions.db) cursor conn.execute( INSERT INTO suggestions (content, category, priority, user_id, contact, created_at) VALUES (?, ?, ?, ?, ?, ?), (content, category, priority, user_id, contact, datetime.now().isoformat()) ) suggestion_id cursor.lastrowid conn.commit() conn.close() return suggestion_id生产环境的话这张表可以加索引优化查询比如按category和created_at建联合索引方便后续做统计分析。如果建议量很大还可以考虑分表或者用消息队列异步落库避免提交时等待时间过长。5. 实测中的意外情况智能体什么时候会乱调用工具5.1 用户随口一说就被当成建议提交了工具跑通之后我拿一批真实对话做了测试发现最典型的问题就是误触发。用户说这个功能做得还行就是有点慢本意可能只是吐槽智能体却调用了提交工具。还有用户问你们有没有夜间模式这是个询问不是建议但智能体也提交了。这个问题的根因在于工具描述里的触发条件不够精确。我最初的描述是当用户表达对产品的意见时调用这个范围太宽了。后来改成当用户明确表达想要提出改进建议、功能需求、体验反馈或内容纠错时调用用户只是询问功能是否存在、或单纯表达情绪时不要调用误触发率明显下降。但光改描述还不够我还在智能体的系统提示词里加了一条约束调用提交工具前先确认用户是否有明确的提交意图。如果不确定可以先问一句你是想把这个作为建议提交吗。这条约束让智能体多了一步确认虽然多一轮对话但避免了大量误提交。5.2 用户中途改主意工具已经调用了怎么办还有一种情况是用户说我提个建议啊算了不说了智能体已经把工具调用了。这种场景处理起来比较棘手因为工具调用是即时发生的没有撤回机制。我的做法是在执行函数里加一个内容有效性判断如果content明显是算了不说了这类无意义内容就返回一个提示让智能体引导用户重新表达。更稳妥的方案是在智能体层面做意图确认也就是上面说的先问一句再调用。这两种方案可以叠加使用意图确认负责拦截大部分误触发内容校验负责兜底。实测下来叠加之后误提交基本可以控制在可接受范围内。5.3 参数缺失时的追问策略用户说提个建议但没说具体建议什么这时候content是空的。智能体需要追问但追问的方式有讲究。如果直接问请填写建议内容太生硬。我让智能体用更自然的方式追问比如好的你想建议哪方面呢说说看。这个追问话术写在智能体的提示词里工具本身只负责在content为空时返回错误提示。这里有个经验工具返回的错误提示要写成给智能体的指令而不是给用户的错误信息。比如返回建议内容不能为空请引导用户补充具体内容智能体看到之后就知道该追问用户而不是把这句话原样念给用户听。6. 让工具更好用几个提升提交体验的细节优化6.1 自动分类的准确率怎么提上去category参数虽然设成选填但智能体大部分时候都会填。我统计了一下自动分类的准确率大概在七成左右主要错在功能建议和体验问题的边界上。比如希望搜索更快一点这算功能建议还是体验问题其实都说得通。提升准确率的办法是在参数说明里给更具体的判断标准。我后来把说明改成用户要求增加新能力、新选项的填功能建议用户抱怨现有功能不好用、太慢、太复杂的填体验问题用户指出内容有事实错误、错别字的填内容纠错。加了这些例子之后分类准确率提升到八成五以上。剩下的模糊地带填其他也不影响后续处理人工分拣的时候再看一眼就行。6.2 提交成功后的反馈话术设计用户提交完建议之后智能体怎么回复直接影响用户的感受。我试过几种话术最后定下来的是建议已提交编号XXX。感谢你的反馈我们会认真评估。这句话包含三个信息提交成功、有编号可追溯、表达感谢。编号很重要用户后续如果想追问进度可以凭编号查询。有些开发者喜欢在回复里加一句我们会尽快处理我不建议这么写因为尽快是个模糊承诺做不到反而伤用户感情。不如老老实实说会认真评估评估不等于采纳用户也能理解。6.3 防止重复提交的小机制同一个用户可能在短时间内反复提交相似的建议比如连点几次或者换个说法再说一遍。我在存储层加了一个简单的去重逻辑查询该用户最近24小时内是否有内容相似度高的建议如果有就返回你之前提过类似的建议我们已经记录啦不再重复入库。相似度判断用简单的文本比对就行比如去掉标点后比较前50个字符是否一致。不用上复杂的语义相似度模型那个成本太高而且容易误判。这个机制上线之后重复建议的数量下降了不少后续处理的时候清爽很多。7. 从工具到闭环建议数据后续怎么用工具只负责收集收集来的数据怎么用才是价值所在。我在存储层之上加了一个简单的统计脚本每天跑一次输出各类建议的数量分布、高频关键词、提交趋势。这些数据给到产品侧他们能快速看到用户最关心什么。比如有一周统计发现搜索相关的建议突然增多产品侧就去排查是不是搜索功能最近有改动导致体验下降。这种从用户反馈到问题定位的链路比等用户流失了再回头分析要主动得多。工具本身不复杂但它把用户有话说和产品能听到之间的通路打通了这才是加这个MCP工具的真正意义。后续如果要扩展可以考虑在工具里加一个建议状态查询的能力让用户凭编号查询自己的建议处理到哪一步了。不过这涉及权限校验和状态管理复杂度比提交高不少得单独设计。目前先把提交这一环做扎实跑顺了再考虑下一步。我个人在实际开发中的体会是MCP工具的开发难点从来不在代码本身而在于对人的理解——用户会怎么说话、智能体会怎么理解、两者之间的偏差在哪里。把这些问题想清楚了代码就是水到渠成的事。