WebView里跑AI Agent?MCP协议我扒了一层皮
WebView 里跑 AI AgentMCP 协议我扒了一层皮前阵子接了个需求甲方说——“用户问’帮我查一下上个月项目文档里提到的那个 API 密钥过期时间顺便发个邮件提醒相关同事’你让我家 AI 能干这事不”我当时心想RAG 就够了啊。查个文档嘛咱之前不是写过 WebView 里跑 RAG 那套么。结果仔细一琢磨不对。查 API 密钥过期时间这步 RAG 确实能搞定。但“发个邮件提醒同事”——这不是检索能解决的问题了。这是要 LLM 调用邮件系统、查通讯录、然后真的给你把邮件发出去。RAG 只能查不能干。所以那条路走不通。那咋办呢Agent。MCP 到底是个啥一句话的事儿先简单说下 MCPModel Context Protocol。你如果看过 Anthropic 的文档会发现他们把 MCP 说得跟天书似的。说白了其实就是——让 LLM 能主动调外部工具的一套标准化协议。类比一下就懂了前端通过 API 调后端 → LLM 通过 MCP 调工具就这么简单。以前你做 Function Calling得你手写死每个函数的定义和参数格式。MCP 的好处是LLM 自己跑去发现有哪些工具可用每个工具怎么用、参数长啥样它自己读。我第一次看官方文档的时候心想这不就是给 AI 写的 OpenAPI 规范吗还真就是这么回事。翻车一Tool Call 流式中断——AI 还没搞完用户已经懵了我一开始想得挺美。LLM 调个工具、返回结果、继续跟你聊——这不就是聊着天顺手干点事么结果第一个坑就砸我脸上了。场景特简单用户问帮我查下周的天气预报顺便定个闹钟。LLM 收到了开始流式输出“我这就帮你查天气……”正说着呢LLM 推理到一半触发了 tool call——它跑去调天气 API 了。问题来了流式输出这时候怎么处理我当时想那就把中间状态也流出去呗“正在查询天气……正在设置闹钟……”你以为用户会觉得哇它在干活错。用户看到的就是一串碎碎念。而且 LLM 查完天气之后返回来继续生成中间那段 tool call 的文本就跟正文搅在一起体验稀碎。你想想你跟同事说帮我把这个文件发一下然后同事一边发一边口述我正在打开邮箱正在填收件人正在点发送按钮——是不是很傻我的解决方案其实挺简单粗暴tool call 执行期间流式输出暂停攒齐结果之后一次性渲染。用户只看得到我已经查好天气了下周三天晴20-25度。闹钟也设好了明早 7 点。这块我翻了好几版才定下来中间还试过把 tool call 状态做成 loading 动画但小模型反应太快loading 一闪而过反而更奇怪。翻车二安全模型——你让 LLM 调浏览器 API它真敢调这个坑说起来都是泪。一开始我图省事把localStorage、Cookie、甚至fetch都注册成了 MCP tool。心想反正跑在浏览器里能出啥事结果 LLM 真敢干。测试的时候我让它帮我查一下最近一条网络请求的状态码结果它自己调了 fetch 发了个外部请求那个接口恰好是个没鉴权的内网服务差点把内部数据带出去。不是我拦得快甲方那边就炸了。还有个骚操作——我注册了一个读取本地文件的工具通过 input file 选择器但没做鉴权。LLM 在对话里诱导用户传敏感文件。虽然是用户自己点的选择器但工具本身没做任何校验。那后面怎么收场的呢搞了三道卡第一道白名单——每个 tool 注册的时候必须显式声明它能干什么、不能干什么不是你想调啥调啥。第二道外发请求强制走代理——你想调哪个 URL得过我这一关。第三道敏感操作加二次确认——涉及读写用户数据、发消息、改配置的先弹确认框用户点了确定再执行。这么一套下来LLM 再想乱来也翻不出什么浪花了。说起来浏览器本身的安全策略也救了我好几次。比如 LLM 想读本地磁盘文件浏览器直接拦了。但你不能全指望浏览器自己的代码也得设卡万一哪天浏览器厂商抽风改了个策略呢翻车三LLM 连 JSON 都填不对这可能是最让我无语的一个坑。MCP 的 tool call 本质上是一个 JSON-RPC 请求。LLM 返回的格式长这样{tool:send_email,params:{to:zhangsancompany.com,subject:API密钥过期提醒,body:请及时更新密钥}}看起来挺规整对吧但实际跑起来LLM 返回的东西千奇百怪函数名拼错send_emial少了个 i参数类型搞反to传了个数组但接口定义是 string少传必填参数、多传不存在的参数最离谱的一次它返回了一整段自然语言描述而不是 JSON“我想调用 send_email 工具把邮件发给 zhangsan……”我一开始的方案是发现格式不对就让 LLM 自己重试。结果它每次用同样的错误格式再试一次无限循环。绝了。最终怎么解决的呢搞了两层兜底。第一层运行时 Schema 校验——参数不对直接抛结构化错误告诉 LLM 具体哪里错了别让它猜。“你传的to字段是个数组但我期望的是 string重来。”第二层万能 fallback 参数填充——比如必填参数没传我不直接拒绝而是从上下文里试着猜。收件人没填看看对话历史里有没有提到过名字。时间没给用当前时间兜底。猜对了皆大欢喜猜错了用户自己改就是了。代码大概长这样——核心逻辑其实没几行主要是校验→猜→过滤三步走// 简化的参数校验代码functionvalidateToolCall(toolCall,schema){consterrors[];for(const[key,def]ofObject.entries(schema.properties)){if(def.requiredtoolCall.params[key]undefined){// fallback: 尝试从对话上下文推断constinferredinferFromContext(key,dialogHistory);if(inferred){toolCall.params[key]inferred;}else{errors.push(缺少必填参数:${key});}}}// 过滤多余参数LLM 最爱传一些不存在的字段for(constkeyofObject.keys(toolCall.params)){if(!schema.properties[key]){deletetoolCall.params[key];}}returnerrors;}注释里写了过滤多余参数那行——你可能觉得这步多余但你真不知道 LLM 会传什么奇怪的字段进来。我有一次 LLM 传了个color: blue到发邮件工具里我到现在都没搞明白它怎么想的。这块我也没完全搞明白最优解。目前这套在大部分场景下能跑但偶尔还是会翻车。有更好的方案欢迎评论区聊聊。什么时候别用 Agent翻了仨大跟头之后说句不该说的——不是所有需求都值得上 Agent。有些场景 RAG 就够了硬上 Agent 纯属给自己找事还多个翻车点。我的判断标准其实就一句话单步查询 → RAG多步执行 / 跨系统操作 → Agent打个比方“查一下文档里 API 密钥什么时候过期”——RAG 完事了。但查过期时间 找出谁在用 发邮件通知 抄送 leader——这就该上 Agent 了。中间每一步都有可能要调不同的工具、做不同的决策Agent 的价值就在这。你想想是不是这个理别为了用 Agent 而用 Agent。你们在项目里遇到过什么让 LLM 翻车的奇葩 tool call评论区聊聊我最近在收集翻车素材写续篇说不定下篇就有你的案例。