ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

给pi agent按需接入MCP:权限开关与人工介入的工程实践

给pi agent按需接入MCP:权限开关与人工介入的工程实践 先亮个结论给 pi agent 接 MCP 这事儿协议和代码都算不上门槛真正麻烦的是——你希望它在什么时候用工具、用什么权限用、用完你怎么收场。最近我花了两周时间把手头一个长期在线的 pi agent 从“裸奔”状态改造成了带 MCP 接入的样子最大的体会只有一句难点不在“接入”而在“按需接入”这四个字。MCPModel Context Protocol现在基本成了 AI 应用接外部工具的事实标准原理不复杂但落到 pi agent 这种需要长期运行、自主决策的智能体上就冒出一堆常规教程不会讲的坑。如果一把梭把所有 MCP server 常驻挂上短期看挺爽时间一长必然出问题上下文被工具输出塞满、token 哗哗烧、工具误调用的风险直线上升甚至会出现 AI 在你不注意的时候自己操作了一堆东西、你根本不知道它在干嘛的恐怖时刻。这篇文章就用我踩过的坑聊聊怎么给 pi agent 设计一套按需开启的 MCP 接入层核心就一个词人工介入。1. 先把为什么理顺给 pi agent 装 MCP难点到底在哪1.1 MCP 没你想的那么玄它是给 AI 用的“USB 接口”很多朋友第一次接触 MCP容易被一堆术语劝退。Model Context Protocol听起来很唬人实际上就是一个规范它规定了 AI 应用MCP Client怎么发现外部能力MCP Server、怎么调用这些能力、结果用什么格式返回。如果打个生活化的比方MCP 之于 AI 就像 USB-C 接口之于手机——早期的手机每个品牌一种充电线想接什么外设都得单独做适配有了统一标准之后一个口就能接显示器、硬盘、键盘、耳机。MCP 就是给 AI 世界的“外设”定了一个统一的接口规范让 agent 不需要为每个工具单独写一套调用逻辑。这个标准一旦统一生态就开始指数级增长了。你去看现在的热词就知道大家玩得多嗨playwright MCP 让 AI 直接操作浏览器chrome devtools MCP 让 AI 读取页面调试信息有人把 cheat engine 都桥接成了 MCP server让 agent 能读游戏内存、改数值还有更贴近业务场景的比如同花顺 MCP 给你喂实时行情数据ruoyi-vue-pro 这类后端框架也有人做了 MCP 合并功能让 AI 直接查询数据库表结构。可以说现在几乎没有哪个热门工具找不到对应的 MCP server。但请注意这里藏着一个很容易被忽略的事实MCP 只解决了“能不能调用”的问题它完全不负责“该不该调用”。USB-C 口统一了物理标准不代表你应该把所有外设永远插在电脑上不拔下来。MCP 也是一样——它在协议层面没有给你做任何权限控制、频率控制、场景控制这些全得靠 agent 侧自己做。1.2 难点不在“能连上”而在“什么时候让它连”我先说说自己最开始犯的错。第一次给 pi agent 接 MCP 时我图省事直接把所有 server 全部常驻挂载文件系统读取、本地命令执行、浏览器自动化、还有几个业务 API全部在 agent 启动时就完成握手工具列表一次全塞给模型。结果呢表面上 agent“能力爆棚”实际上问题接二连三地来。第一个是安全问题。工具一旦常驻就相当于在你家大门上挂了钥匙而且任何人都能拿——只要模型在某个上下文里觉得“有必要”它就可能调用一个权限很大的工具。我遇到过 pi agent 为了更好地“整理”文件直接自作主张把一堆日志文件挪到了回收站幸好我当时的命令工具带了一个“只能读不能写”的开关否则后果可想而知。第二个问题是成本问题。MCP server 的工具描述、返回结果都会占用上下文窗口常驻十个工具就多出几千 token 的基础开销每次对话都在燃烧 tokens。第三个问题是失控感——AI 一旦开始频繁调用工具你作为人类完全不知道它的决策路径慢慢地它就会变成一个你无法理解、无法审计、也无法信任的黑盒。所以我把“按需开启”当成了刚需而不是一个优雅的加分项。所谓“人工介入的艺术”核心思路就是不要追求全自动也不要把 AI 当一个全靠自律的好员工而是要给它设计一套“可被人工接管”的运行机制。按需开启的 MCP本质上是在 AI 的自由度外面加一个可控的阀门——这个阀门由谁控制、什么时候开、开多大就是这套设计的灵魂所在。2. 动手前先盘清三张清单能力、角色、开关粒度2.1 能力清单到底要接哪些工具在写任何代码之前先列一张纸我的 pi agent 究竟需要哪些能力我建议按“使用频率 × 危险程度”来给候选工具分类而不是按“哪个好玩就接哪个”。拿我自己的例子来说高频低危读文件、查时间、做数学计算、查天气、搜索本地笔记。这些工具几乎每次对话都用得上危险程度很低可以默认开启或者用一个非常宽松的开关管理。低频高危执行 shell 命令、删除/移动文件、调用外部接口写数据、发邮件、操作数据库。这些工具用得不多但一旦误操作就是事故。它们必须默认关闭只有人工确认后才临时开启。中频中危浏览器自动化playwright MCP、抓取网页内容、调用第三方 API。这类工具介于两者之间按场景开启比如我说“帮我爬一下这个页面”它才去连接对应的 browser MCP server。这张清单的价值在于它把“能不能调”这个抽象问题变成了“什么场景下可以调”的具体规则。后面设计按需开启机制的时候你只需要照着清单配置即可不需要在代码里临时纠结。2.2 角色清单pi agent 是 client 还是 server第二张清单容易被人忽略你的 pi agent 到底扮演什么角色。MCP 架构里有两个角色——Client 和 Server绝大多数人默认 pi agent 就是 Client去连接别的 MCP server。这没错但它其实还可以反过来把自己的能力封装成一个 MCP server暴露给其他 AI 工具调用。这两种角色对“按需开启”的要求完全不同。作为 Client 时你需要控制的是“agent 什么时候去连别人的 server”重点在调用侧的开关作为 Server 时你需要控制的是“别的 AI 什么时候能连我”重点在服务侧的鉴权和启停。你看现在的热词其实很能说明问题trae IDE 搭载 burp suite MCP server让 AI 直接操控 burp suite 做安全测试codex 接入蓝湖 MCP让 AI 能拉设计稿标注cursor 配浏览器 MCP让 IDE 里的 AI 能实时操作页面。这些场景里AI 或者 IDE 是 Clientburp suite、蓝湖那些工具是 Server。反过来如果你的 pi agent 本身就有一堆独特能力你也可以把它做成一个 Server暴露给编辑器或者其他 agent 使用。我的建议是一开始先只做 Client 角色因为按需开启的最简单形态就是“client 主动连接连完即走”。Server 角色涉及的鉴权、会话管理、并发控制复杂得多等你的接入层稳定了再考虑不迟。2.3 开关粒度三档状态设计第三张清单是开关的粒度。很多人以为“按需开启”就是一个 bool 值——要么开要么关。实际用下来两档根本不够用。我最终采用的是三档状态机off / manual / auto。off工具完全不可用。agent 在请求规划阶段就不会看到这个工具自然也就不会去调用它。这是默认状态尤其对所有高危工具。manual工具可被 agent 调用但每次调用前都需要人工确认。这个确认可以是聊天里的一句回复、一个审批按钮或者一次手动敲门的信号。auto工具可以自动调用但通常配一个次数限制或者时间窗口比如“5 分钟内最多调用 3 次”“今天下午可以自动执行文件读取”。窗口一过自动回落到 off 或 manual。这个三档设计把“人工介入”从一道选择题变成了一个渐变旋钮完全信任时拧到 auto半信半疑时停在 manual完全不放心就拧回 off。后面我会给出具体代码但现在你只需要记住一个原则默认宁可多锁不能少锁。每次开启一个 MCP都是从 off 往 auto 方向推出过一次事故的工具至少一个月内不要再给它 auto 权限。3. 落地实操给 pi agent 装上“按需开启”的 MCP3.1 基础设施准备server 端与 client 端说完了理论直接上手。我用的 pi agent 是 Python 写的基于一个轻量的 agent 框架MCP 部分采用官方 Python SDK。第一步是准备一个 MCP server 作为测试对象。你可以直接用 FastMCP 写一个本地 server比如给它加两个工具一个读文件、一个写文件。核心代码大概长这样from mcp.server.fastmcp import FastMCP mcp FastMCP(pi-local-tools) mcp.tool() def read_file(path: str) - str: 读取本地文件内容用于信息检索和代码审查 with open(path, r, encodingutf-8) as f: return f.read() mcp.tool() def write_file(path: str, content: str) - str: 写入文件注意此操作会覆盖原文件仅在人工确认后使用 with open(path, w, encodingutf-8) as f: f.write(content) return fwritten: {path} if __name__ __main__: mcp.run()这样一个 server 就起来了。MCP 官方的传输方式主要有两种一种是 stdio它和你本地 agent 进程直接通过标准输入输出通信适合跑在本机、和 agent 同生命周期优点是无网络开销、启动快缺点是无法被远程访问另一种是流式 HTTPstreamable HTTP通过 WebSocket 或 HTTP 长连接通信地址通常长这样wss://api.example.com/mcp/?token...。这个带 token 的 wss 地址就是远程 MCP server 的入口你在 agent 侧配置连接时需要把它和对应 token 一起配好。如果你想在本地快速验证直接跑 stdio 模式最简单如果 pi agent 部署在服务器上、MCP server 在另一台机器那就用 wss。有一点必须提醒wss 地址里带着 token一定要当成密码对待别随手贴到日志、截图或者公开代码仓库里。我见过有人把真实 token 发在群聊里调试结果被别人拿去调了一晚上的付费 API账单让他欲哭无泪。3.2 核心代码带开关的 MCP 调用层server 端不是重点重点在 pi agent 侧怎么实现“按需接入”。我没有直接用官方 client 的裸连接而是包了一层 ToolRouter把所有 MCP 工具的调用都收口到一个带状态管理的注册表里。先看代码再解释为什么这样设计from enum import Enum from typing import Any, Callable import asyncio class AccessLevel(str, Enum): OFF off MANUAL manual AUTO auto class McpToolRegistry: 维护所有 MCP 工具的开关状态和调用逻辑 def __init__(self) - None: self._tools: dict[str, dict[str, Any]] {} def register( self, name: str, invoke: Callable[..., Any], level: AccessLevel AccessLevel.OFF, allowlist: list[str] | None None, max_auto_calls: int 0, ) - None: self._tools[name] { invoke: invoke, level: level, allowlist: allowlist or [], max_auto_calls: max_auto_calls, auto_used: 0, audit: [], } def effective_level(self, name: str, args: dict[str, Any]) - AccessLevel: 根据白名单和参数计算本次调用的实际权限级别 tool self._tools.get(name) if not tool: return AccessLevel.OFF if tool[level] AccessLevel.AUTO: # 白名单机制即使 auto 状态超出参数白名单也拒绝 if tool[allowlist] and not set(args.keys()).issubset(set(tool[allowlist])): return AccessLevel.MANUAL if 0 tool[max_auto_calls] tool[auto_used]: return AccessLevel.MANUAL tool[auto_used] 1 return AccessLevel.AUTO return tool[level] async def call(self, name: str, args: dict[str, Any], confirm: Callable[[], bool]): level self.effective_level(name, args) if level AccessLevel.OFF: raise PermissionError(ftool {name} is disabled) if level AccessLevel.MANUAL: # 人工介入点在这里弹窗、或者等待用户在聊天里确认 ok await confirm() if not ok: raise PermissionError(fuser rejected tool {name}) tool self._tools[name] result await tool[invoke](**args) tool[audit].append({args: args, level: level.value}) return result这段代码有几个关键点我逐个讲。第一为什么不用 bool 而用枚举因为枚举能把“手动确认”和“自动放行”区分开而且未来扩展状态机很方便。你以后如果觉得 three-state 不够还能加个manual_once、scheduled_window枚举类型扩展起来不会破坏既有逻辑。第二effective_level这个方法的出现是因为一个常见事故你以为工具处于 auto 状态但它接收的参数里包含一个不在白名单内的危险参数比如文件名是/etc/passwd或者--force这时候绝对不能让 AI 直接执行。白名单机制是在开关之外加的一道保险丝哪怕你已经手动拧到 auto参数越界照样弹回 manual。第三confirm参数是一个回调函数它把“人工介入”这个动作抽象成了依赖注入。你在本地终端里跑可以让它弹一个 y/n 输入你在服务端跑可以让它往聊天群里发一条待确认消息你在 Web 界面跑可以让它变成一个待审批按钮。这个抽象让你的人工介入方式完全解耦想怎么加都行。实际用的时候我建议把registry.call作为唯一入口agent 的 tool-calling 循环里所有 MCP 工具调用都走这里不要跳过。一旦绕过审计日志就不完整了出问题的时候你根本没法复盘。3.3 人工介入位手动确认的三种触发方式代码有了接下来要考虑的就是“人工介入”本身怎样做到位。我最开始的设计是agent 要调用 manual 状态的工具时直接向 chat 输出一行“需要确认请回复 y”然后阻塞等待。这在交互式调试里能用但 pi agent 经常是跑在后台、无人值守的光在终端里等输入并不现实。我后来实现了三种触发方式效果都不错聊天指令触发在对话界面里输入/mcp on tool或者/mcp allow tool --args-filexxx相当于手动指定“这次我允许你调用”。实现上就是给 registry 里的某个 tool 临时置位MANUAL_ONCE调用一次后自动回落。Web 控制台触发我给 pi agent 加了一个极简的本地 HTTP 控制台页面上一排工具开关点一下就从 off → manual → auto 循环切换同时能看到最近 20 条调用审计。这个方案最直观也最适合远程跑着 agent 的场景。调试的时候开一次日常关掉避免端口暴露。热键/手势触发我见过有人用键盘快捷键给本地 agent 发一个 SIGUSR1 信号agent 收到信号后把默认级别临时提高到 auto 持续 10 分钟之后自动降回 manual。这种触发方式适合做演示或者当你明确知道接下来一轮操作需要 AI 放开手脚时提前开一个时间窗。说句实话手动确认的过程如果做得太烦你很快就不想用了。所以我的经验是每个工具至少要有两种触发方式一种适合交互式场景一种适合无人值守场景。比如聊天输入适合人在电脑前Web 控制台适合人不在电脑前信号量适合想要极客风味的人。按自己习惯选即可。3.4 连接参数调优超时、重试与并发MCP 连接不是配好就能一直稳着用的尤其是 wss 方式网络环境稍微一抖就出幺蛾子。我整理了几个关键参数的经验值供参考参数建议值说明connect_timeout10s建连时间超过 10 秒基本可以判定网络问题不硬等request_timeout60s工具单次调用的超时读大文件或跑浏览器操作时需要放大heartbeat_interval30swss 长连接保活心跳防止空闲连接被服务端断开reconnect_max_retries3 次超过 3 次重连失败就进入降级模式不再自动重连concurrent_calls2同一个 MCP server 的工具并发数太多容易打爆服务端auto_calls_per_window5 次/10 分钟auto 状态下的最高调用频率防止突发失控这些参数不要照搬要根据你的实际工具跑一下压测。特别提醒request_timeout这个参数我第一次用 playwright MCP 的时候默认 30 秒超时结果 agent 操作页面快照时经常半路断开后来调到 90 秒才正常。超时太短容易误判失败超时太长又会让错误调用卡住整个 agent 的决策循环适中的值得靠实践慢慢调。还有重连逻辑千万别做成无限重连。我踩过一次坑某个远程 MCP server 临时维护结果我的 pi agent 每 5 秒重连一次日志刷了整整一个小时把上下文都撑爆了。后来加了指数退避第一次等 1 秒第二次 2 秒第三次 4 秒最多 30 秒间隔连续 5 次失败就彻底停掉这个连接并告警。这个行为也非常适合按需开启的场景——一个“按需连接”的 client遇到连不上的 server 本来就该果断放弃而不是一遍遍啃硬骨头。4. 典型问题排查实录与避坑清单4.1 wss 长连接闪断、token 过期怎么办先说连接问题。wss 方式的 MCP 有个特性连接是有状态的一旦断开服务端可能直接放弃这个会话需要重新握手。如果你的 pi agent 在跑一个长任务中间某个 MCP server 闪断最常见的结果就是工具调用直接抛异常而且抛出的是一个很含糊的connection closed。我排查了几轮才发现原因服务端的空闲连接超时设的是 60 秒而我的 agent 在等待用户确认时超过了这个阈值连接早被服务端回收了。解决方案有两个层面。第一层是保活客户端每 30 秒发一个 ping 或者一个轻量级的resources/list请求让连接保持活跃。第二层是体面降级检测到连接断开后不是马上重连拉满日志而是进入一个“等待用户指令”的状态——因为如果断开的工具本来就需要人工确认那正好让人介入一下再连。token 过期的问题在远程 server 上更常见每次重连时如果发现鉴权失败通常是 401 或特定的错误码一定要触发 token 刷新流程而不是无限重试。我在 registry 里给每个工具额外配了一个on_auth_error回调专门负责去拿新 token拿不到就自动降级到 OFF绝不做任何未鉴权的调用。4.2 工具调用失控AI 忠实地执行了“危险操作”这是我最想强调的一个坑所谓工具失控很多时候并不是 AI 发疯了而是你给了它错误的许可。有一次我临时把write_file切到 auto以便让 agent 帮我批量生成配置文件。结果它在一次上下文里连续写了 30 多个文件把某个目录下的旧配置全部覆盖了。回头看日志每一步都是按照我的指令“正确”执行的但没有一步经过人工审查因为它处于 auto 状态。从那以后我养成了一个习惯任何有覆盖、删除、移动语义的工具不管多简单默认永远挂在 manual永远不给 auto。因为这种工具一旦误操作恢复成本极高而人工确认一次的代价其实非常低。如果你真的要给它 auto也一定要配上“模拟运行”的 dry-run 模式——先让 agent 汇报“我准备删除这些文件、改动这些内容”确认后才真正执行。很多现成的 MCP server 不带 dry-run你需要自己在 invoke 外面包一层或者用一个默认改不了数据的安全代理来封装。4.3 上下文被工具结果塞满MCP 工具返回的结果是直接拼进上下文的一个不小心你的 agent 就“记忆超载”了。我最夸张的一次是让浏览器 MCP 抓一个大型页面返回了大概 8 万字符的 HTML直接把上下文窗口占掉一大半后面 agent 开始胡言乱语把前面的指令都忘了。这属于典型的上下文漂移。解法有三第一在工具调用返回之前做结果清洗比如只截取前 2000 字符或者只返回页面里的正文文本第二给模型提供的是“结果摘要”而不是“完整结果”第三如果确实需要完整数据让 agent 把数据写到临时文件返回的只是文件路径。这三个方案可以叠加用。我自己现在是默认走摘要模式只有显式要求“完整输出”时才放开。4.4 避坑清单命名冲突、会话串号、SDK 版本最后把几个零碎的坑记下来建议你配置的时候逐条对照。工具命名冲突。两个 MCP server 可能都注册了read_file这样 registry 里后注册的会覆盖先注册的导致你按需开启了一个工具、调用的却是另一个实现。我的解决办法是在注册时强制带上 server 前缀比如fs_read_file、browser_read_page或者在 registry 里做重名检测并启动时报错。会话串号。如果 pi agent 是多用户或双会话的A 会话确认了一个工具调用B 会话可能也能看到并复用确认状态。这很危险。我的 registry 里每个会话都有独立的工具状态副本互不共享宁可多占一点内存。SDK 版本不匹配。MCP 协议还在快速迭代server 端用 v1.xclient 端用 0.x经常出现握手失败、工具响应格式对不上的问题。升级的时候尽量 server/client 一起升升级后第一件事就是跑一遍冒烟测试。日志泄露参数。工具参数里可能包含文件路径、token、请求体内容这些都会被审计日志记录。日志如果打到公共平台等于间接泄露了敏感信息。审计日志至少要做字段脱敏把路径中的用户名、token 内容替换成***。手工确认没有超时。manual 状态等着用户确认如果用户一直没回应agent 就会空转卡住。我给 confirm 加了一个 10 分钟超时超时默认拒绝并让 agent 继续做别的事情而不是沉到底。这些坑单个看都不起眼但任何一个都很可能毁掉你一整天的调试心情。把它们写进你 agent 的 README 或者设计文档里比事后在群里问“为什么我的 MCP 不工作”要高效得多。5. 我的一些个人体会折腾完这套按需开启的 MCP 接入层之后我最大的感受是给 pi agent 加 MCP 本身并不难难的是让它在“能力强大”和“可以被信任”之间找到一个平衡点。模型自己不会判断何时该用哪个工具、哪个操作会带来不可逆的后果这些边界必须由人设计好再由人在关键时刻介入。你做得越细AI 的自主性才越安全。我一直觉得自动化的尽头不是彻底取代人的判断而是设计一套“可被无缝接管”的自动化。按需开启的 MCP 就是这套理念的一个具体实现——平时它安静地待在 off 状态需要时你拧到 manual 或者 auto用完再拧回来。就像开手动挡车虽然多了一个换挡的动作但你对这辆车的掌控感是开全自动挡完全比不了的。最后分享一个小技巧新接入任何一个 MCP server先让它默认 off 跑一周期间你需要用时手动开用完马上关。一周之后打开审计日志看看哪些工具是你真正高频使用的哪些一次都没碰过。高频的自然而然升级成 auto没碰过的一律保持 off不心疼。这个习惯帮我省下了大量上下文开销也让 pi agent 真正变成了一个“知道什么时候该用什么工具”的聪明助手而不是一个手里拿着锤子看什么都像钉子的蛮干机器。
返回列表