1. 为什么"能聊天的 AI"和"能干活的 AI"是两回事
很多人第一次接触 AI 编程智能体,脑子里想的都是"我让它写个登录页,它给我吐出来一段代码"。这个预期本身没错,但真正落到商业项目里,你会发现光会吐代码远远不够。它得知道你的项目结构、得能读你本地的文件、得能调用你的构建脚本、得能在你现有的 IDE 工作流里插进去而不是另起炉灶。这就是"聊天机器人"和"编程智能体"之间那道最深的沟。
MCP 协议(Model Context Protocol)出现的意义,恰恰就是来填这道沟的。你可以把它理解成 AI 模型和外部世界之间的一套"标准插座"——以前每个工具都要给每个模型单独写一套对接代码,现在大家统一插同一个口。这个类比不精确但足够直观:MCP 之于 AI 工具调用,有点像 USB-C 之于充电线,接口统一了,生态才能滚起来。
我写这篇东西的出发点很实际。过去大半年我一直在做 AI 编程智能体的落地,从最早的 LangChain 手搓 Agent,到后面用 LangGraph 做编排,再到接入 MCP 把本地 IDE 能力暴露给模型,中间踩的坑能写一本书。网上讲 MCP 概念的文章很多,但真正讲"商业级"三个字怎么落地的很少——什么叫商业级?我的定义是:能扛住多人并发、能控制权限边界、出错能恢复、成本能算得清。这四条缺一条,都只能算玩具。
这篇文章适合三类人看:一是已经会用 LangChain 写 Demo,但不知道怎么往生产推的开发者;二是团队里负责 AI 工具链选型的技术负责人;三是想搞清楚 MCP 到底解决了什么工程问题的架构师。我会尽量少讲虚的,多讲我实际怎么配、怎么调、怎么排错的。
2. MCP 协议到底在协议什么:从"函数调用"到"能力插座"
2.1 传统 Function Calling 的三个硬伤
在 MCP 之前,让模型调用外部工具的主流做法是 Function Calling。你在请求里塞一堆工具定义,模型决定调哪个、传什么参数,你本地执行完再把结果塞回去。这套机制能跑,但一旦工具数量上去、工具来源变杂,问题就来了。
第一个硬伤是工具定义和模型强耦合。你给 GPT 写的工具 schema,换到 Claude 上得改一遍,换到本地开源模型又得改一遍。每个模型的 tool 格式、参数约束、返回结构都有细微差别,维护成本随模型数量线性增长。
第二个硬伤是工具发现是静态的。你必须在每次请求里把所有工具定义都带上,哪怕这次对话根本用不到文件系统。工具一多,光工具定义就吃掉几千 token,还没开始干活钱就烧了一半。
第三个硬伤是没有标准化的权限和生命周期管理。工具能读什么、能写什么、超时多久、失败了怎么重试,全靠你自己在业务代码里硬编码。团队一多人一多,这套东西就散架了。
2.2 MCP 的三个核心抽象
MCP 用三个概念把上面这些问题一次性收拢了:Resources(资源)、Tools(工具)、Prompts(提示模板)。
Resources 是"可读的东西",比如你项目里的某个文件、数据库里的某张表、某个 API 的返回结果。它对应的是"读"这个动作,模型可以列出有哪些资源、读取某个资源的内容。
Tools 是"可执行的动作",比如运行测试、创建文件、执行 git 命令。它对应的是"写"或者"副作用"这个动作,模型调用它会产生实际影响。
Prompts 是"预置的交互模板",比如"帮我 review 这段代码"这种固定套路,可以封装成模板让用户一键触发。
这个划分的价值在于:读和写的权限可以分开管。你可以让模型自由读项目文件,但写操作必须经过人工确认。这在商业场景里是刚需,后面讲权限那节会展开。
2.3 传输层:stdio 和 SSE 的取舍
MCP 目前主流的传输方式有两种:stdio和SSE(Server-Sent Events)。
stdio 就是标准输入输出,MCP Server 作为一个子进程跑在你本地,通过管道和客户端通信。优点是简单、快、没有网络开销,适合本地 IDE 场景。缺点是只能本机用,没法多客户端共享。
SSE 是走 HTTP 长连接,MCP Server 作为一个独立服务跑着,多个客户端可以连同一个 Server。优点是能共享、能远程、能做集中式权限管理。缺点是要处理网络、要处理并发、要处理断线重连。
我的经验是:开发阶段用 stdio,生产阶段用 SSE。开发时你一个人一台机器,stdio 启动快、调试方便,日志直接打终端里。上线后如果团队多人共用一套工具能力,SSE 是唯一选择,否则每个人本地都要装一遍环境,版本一乱就是灾难。
这里有个容易忽略的细节:SSE 模式下 MCP Server 是有状态的,每个客户端连接会维持一个 session。如果你的 Server 里存了会话相关的数据,得考虑多实例部署时的 session 共享问题。我一开始没注意,横向扩容后出现"同一个用户两次请求打到不同实例,上下文丢了"的问题,排查了半天。
3. 用 LangGraph 编排智能体:为什么不用裸 LangChain
3.1 LangChain Agent 的失控问题
LangChain 的 Agent 用起来很爽,几行代码就能跑起来一个能调工具的智能体。但你在生产里跑一段时间就会发现,它的执行路径是不可控的。模型决定调什么工具、调几次、什么时候停,你只能通过 prompt 去"引导",没法从代码层面强制约束。
这在 Demo 阶段没问题,在商业场景里是致命的。想象一下:用户问了个模糊问题,模型陷入"调工具-看结果-再调工具"的循环,烧了 20 次调用还没收敛。你既没法中途打断,也没法设置硬性预算上限。
3.2 LangGraph 的状态机思路
LangGraph 的核心思路是把智能体的执行过程建模成一张状态图。每个节点是一个处理步骤,边是转移条件,整个执行过程是可枚举、可干预、可持久化的。
我实际用下来,LangGraph 解决商业场景三个关键问题:
第一,执行路径可控。你可以定义"最多循环 N 次"、"某个工具调用失败后走降级分支"、"涉及写操作必须经过人工确认节点"。这些是代码层面的硬约束,不依赖模型自觉。
第二,状态可持久化。LangGraph 的 checkpointer 机制能把每一步的状态存下来。这意味着用户中途关掉页面,下次回来能接着聊;服务崩了重启,能从上次的 checkpoint 恢复。商业系统里这个能力是底线。
第三,可观测性。每个节点的输入输出都能打点,你能清楚看到模型在哪一步做了什么决策、花了多少 token、耗时多久。出了问题能定位,而不是对着一个黑盒抓瞎。
3.3 一个最小可用的图结构
我常用的一个基础图结构是这样的:入口节点接收用户输入,路由节点判断意图(是闲聊、是查代码、还是要执行操作),查代码走只读工具链,执行操作走"确认-执行-验证"三段式,最后统一汇聚到回复节点。
from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated import operator class AgentState(TypedDict): messages: Annotated[list, operator.add] intent: str pending_action: dict | None confirmed: bool def route_intent(state: AgentState): # 基于最后一条消息判断意图 last = state["messages"][-1] if "执行" in last.content or "运行" in last.content: return "confirm" return "readonly" builder = StateGraph(AgentState) builder.add_node("route", route_intent) builder.add_node("readonly", readonly_chain) builder.add_node("confirm", confirm_node) builder.add_node("execute", execute_node) builder.add_node("verify", verify_node) builder.set_entry_point("route") builder.add_conditional_edges("route", route_intent, { "confirm": "confirm", "readonly": "readonly", }) builder.add_edge("confirm", "execute") builder.add_edge("execute", "verify") builder.add_edge("verify", END) builder.add_edge("readonly", END) graph = builder.compile(checkpointer=memory_saver)这段代码的关键不在语法,在于把"确认"做成了一个独立节点。模型想执行写操作,必须先经过这个节点,这个节点会暂停执行、把待执行的动作抛给前端、等用户点确认。这是商业级和玩具级的分水岭。
4. 把 IDE 能力接进 MCP:文件、终端、LSP 三件套
4.1 为什么是这三样
一个编程智能体要真正"下地干活",最少需要三种能力:读写文件、执行命令、理解代码语义。
读写文件是基础,模型得能看到你的项目长什么样。执行命令是手脚,能跑测试、能装依赖、能启动服务。理解代码语义是大脑的延伸,光看文本不够,得知道这个函数被谁调用了、这个变量是什么类型、这个 import 指向哪里——这就是 LSP(Language Server Protocol)的活。
MCP 官方和社区已经有一些现成的 Server 实现,比如 filesystem server 提供文件读写,shell server 提供命令执行。但 LSP 这块相对薄弱,我实际项目里是自己包了一层。
4.2 文件操作的权限设计
文件操作最容易出事。我见过有团队直接给模型开了项目根目录的读写权限,结果模型一个"清理无用文件"的操作把.env删了。这不是模型的错,是权限设计的问题。
我的做法是白名单 + 路径规范化 + 操作分级。白名单限定模型能碰的目录,路径规范化防止../../这种穿越,操作分级把"读"和"写"分开授权。
import os from pathlib import Path ALLOWED_ROOTS = [Path("/workspace/src"), Path("/workspace/tests")] WRITE_ALLOWED = [Path("/workspace/src")] def safe_resolve(user_path: str) -> Path: p = Path(user_path).resolve() if not any(p.is_relative_to(root) for root in ALLOWED_ROOTS): raise PermissionError(f"path {p} outside allowed roots") return p def write_file(user_path: str, content: str): p = safe_resolve(user_path) if not any(p.is_relative_to(root) for root in WRITE_ALLOWED): raise PermissionError(f"write not allowed for {p}") p.write_text(content, encoding="utf-8")is_relative_to是 Python 3.9+ 的方法,用它比字符串前缀匹配安全得多,能防住src-evil这种前缀相同的绕过。
4.3 终端执行的沙箱化
命令执行比文件操作更危险,因为一条rm -rf就能把事搞大。商业场景里必须沙箱化。
我的方案是容器隔离 + 命令白名单 + 超时熔断。每个会话起一个轻量容器,容器里只挂载项目目录,网络按需开放。命令走白名单,只允许npm、pytest、git这类开发命令,rm、curl、chmod这类直接拒绝。超时设 30 秒,超了就 kill。
# docker-compose 片段 services: agent-sandbox: image: agent-sandbox:latest volumes: - ./workspace:/workspace:rw network_mode: none mem_limit: 2g cpus: 2 read_only: false security_opt: - no-new-privileges:truenetwork_mode: none是关键,默认不给网络。需要装依赖时再临时开一个受控的代理出口,用完就关。no-new-privileges防止容器内提权。
4.4 LSP 接入的实际价值
LSP 接入后,模型能做的事上了一个台阶。比如用户问"这个函数在哪被调用了",模型不用全文搜索,直接问 LSP 要引用列表。问"这个变量的类型是什么",LSP 直接给类型信息。这比让模型读一堆文件去推断准确得多,也省 token。
我用的方案是把 LSP 的textDocument/definition、textDocument/references、textDocument/hover这几个能力包成 MCP Tools。模型需要时调用,不需要时不占上下文。
这里有个坑:LSP Server 启动慢,第一次调用可能要等几秒。我的做法是预热——会话建立时就把 LSP Server 拉起来,后台等它 ready,用户第一次问代码问题时就不用等。
5. 并发、成本、可观测:商业级的三道坎
5.1 并发不是加机器就能解决的
AI 智能体的并发和普通 Web 服务不一样。普通服务一个请求几百毫秒,加机器就能扛。智能体一个请求可能跑几十秒,中间还调好几次模型,占着连接不放。你加机器,成本线性涨,但吞吐不一定线性涨,因为瓶颈可能在模型 API 的速率限制上。
我的做法是分层限流 + 异步化 + 队列削峰。接入层按用户维度限流,防止单用户刷爆;执行层用异步任务队列,请求进来先入队,worker 池按模型 API 的速率限制消费;结果通过 SSE 推回前端。
import asyncio from asyncio import Semaphore # 按模型供应商维度限流 model_semaphores = { "provider_a": Semaphore(20), "provider_b": Semaphore(10), } async def call_model(provider: str, payload: dict): async with model_semaphores[provider]: return await _do_call(provider, payload)Semaphore 的数量要按你实际拿到的速率限制来设,设大了会被供应商限流,设小了吞吐上不去。这个数得压测出来,不能拍脑袋。
5.2 成本控制要细到每次调用
智能体的成本是"模型调用次数 × 单次 token 数"的乘积。两个因子都能优化。
调用次数上,缓存是最大的杠杆。同样的文件内容、同样的查询,结果缓存起来,下次直接返回。我用的是内容哈希做 key,文件变了哈希就变,缓存自动失效。
token 数上,上下文裁剪是关键。不要把整个项目塞给模型,只给相关的文件片段。LSP 在这里又派上用场——模型问某个函数,你只把那个函数和它的直接依赖给它,而不是整个文件。
我还会给每个会话设一个token 预算,超了就降级到更便宜的模型或者直接提示用户。这个预算在 LangGraph 的状态里维护,每次模型调用前检查。
5.3 可观测性:出了问题能定位
商业系统最怕的是"用户说不好用,你不知道哪不好用"。智能体的可观测性要覆盖三层:模型调用层(每次调用的输入输出、token、耗时、成本)、工具调用层(调了什么工具、参数是什么、结果是什么、成功还是失败)、业务层(用户问了什么、最终回复是什么、中间走了哪些节点)。
我用 OpenTelemetry 做统一埋点,trace 串起整个执行链路。一个请求进来生成一个 trace_id,模型调用、工具调用、节点转移都挂在这个 trace 下。出问题时按 trace_id 一查,整条链路清清楚楚。
这里有个实践建议:把 prompt 和模型输出也存下来,但要注意脱敏。用户代码里可能有密钥、有敏感信息,存之前得过滤。我吃过这个亏,日志里存了用户的 API key,被安全扫描扫出来了。
6. 踩过的坑:从 Demo 到生产之间的真实教训
6.1 工具描述写不好,模型就不会用
MCP Tools 的定义里有个description字段,很多人随便写一句就完事。实际上这个描述是模型决定要不要调这个工具的唯一依据。描述写得含糊,模型要么不调,要么乱调。
我的经验是描述里必须包含三样:这个工具做什么、什么时候该用、参数的含义和约束。比如不要写"读取文件",要写"读取指定路径的文件内容,用于查看代码或配置。路径必须是项目内的相对路径,不支持绝对路径和上级目录"。
6.2 错误处理不能只返回 "error"
工具执行失败时,如果你只返回一个{"error": "failed"},模型不知道发生了什么,可能会重试同样的操作,陷入死循环。正确的做法是返回结构化的错误信息:错误类型、错误原因、建议的下一步。
比如文件不存在,返回{"error": "file_not_found", "path": "...", "suggestion": "use list_files to check available files"}。模型看到 suggestion 就知道该换个工具试试,而不是傻乎乎重试。
6.3 上下文窗口不是越大越好
早期我追求把尽可能多的上下文塞给模型,觉得信息越多决策越准。实际跑下来发现,上下文太长反而会让模型"分心",抓不住重点,而且成本飙升。
后来我改成按需加载:初始只给项目结构和当前文件,模型需要看别的文件时主动调工具去读。这样上下文精简,模型注意力集中,成本也降下来了。
6.4 人工确认节点不能省
前面提过写操作要人工确认,这里再强调一次。我见过太多团队为了"体验流畅"把确认环节砍掉,结果出了事故。商业系统里,任何有副作用的操作都必须有确认环节,这是底线不是可选项。
确认的粒度可以调:低风险操作(比如创建新文件)可以批量确认,高风险操作(比如删除、覆盖、执行命令)必须逐个确认。这个分级策略要写进代码,不能靠 prompt 约束。
7. 关于选型的一些个人看法
MCP 生态现在还在快速演进,工具和框架的成熟度参差不齐。我的建议是核心链路自己掌控,边缘能力用现成的。
核心链路指的是智能体的编排逻辑、权限控制、成本核算,这些必须自己写,因为每个业务的需求都不一样,用现成的框架反而束手束脚。边缘能力指的是具体的工具实现,比如文件读写、命令执行,这些有现成的 MCP Server 就用,没必要重复造轮子。
LangChain 和 LangGraph 的关系也要理清。LangChain 适合做单次的链式调用,LangGraph 适合做有状态、有分支、需要持久化的复杂流程。商业级智能体基本都需要 LangGraph 这一层,LangChain 更多是作为底层组件被调用。
最后说个心态问题。AI 编程智能体这个领域变化太快,今天的最佳实践明天可能就过时了。与其追新,不如把权限、成本、可观测这三个地基打牢。地基稳了,上层换什么框架都能接得住。我在实际项目里最大的体会就是:别被新概念带着跑,先想清楚你的业务到底需要智能体解决什么问题,再倒推技术选型。很多时候,一个设计良好的简单方案,比一个花哨的复杂方案更能扛住生产的考验。