ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MCP架构解析:Model-Controller-Protocol分层设计与工程落地

MCP架构解析:Model-Controller-Protocol分层设计与工程落地

1. 这门课到底在讲什么:剥离营销话术,还原MCP在Agentic AI中的真实定位

“【智能代理】Agentic AI | 第六讲 | 第三部分 | MCP | Venkat Reddy AI课程”——这个标题里堆砌了太多容易让人晕头转向的标签。我第一次看到时也下意识点开想“速成”,结果发现课程视频里反复出现的“MCP”既不是某个新出的开源框架,也不是某家大厂刚发布的API,而是一个被严重泛化、反复挪用的概念符号。它在不同讲师口中,有时指代一种架构模式,有时变成一个抽象原则,甚至在某些PPT里直接等同于“让Agent能自己调用工具”这个动作本身。这种模糊性不是偶然,而是当前Agentic AI教学领域的一个典型现象:技术概念尚未沉淀,教学内容已先行包装。

MCP,全称是Model-Controller-Protocol,它本质上是一套分层解耦的设计思想,而非具体代码库。它的核心诉求非常朴素:当一个AI Agent需要完成复杂任务(比如“帮我分析这份财报并生成PPT大纲”),它不能把所有逻辑——理解用户意图、调用财务API、解析PDF、调用PPT生成服务、组织语言——全部塞进一个大模型的提示词里。那样做,错误会像多米诺骨牌一样连锁坍塌。MCP要解决的,就是如何把“谁来决定下一步做什么”(Controller)、“谁来执行具体动作”(Model/Tool)、“它们之间用什么格式说话”(Protocol)这三件事,清晰地切分开。

举个生活化的例子:你请一位经验丰富的项目经理帮你盖一栋楼。他不会自己去和钢筋厂谈价格、不会亲自绑钢筋、也不会手绘每一张施工图。他的角色是Controller——接收你的需求(“我要一栋带花园的三层小楼”),判断当前阶段该做什么(“先找设计师出图”),然后通过标准合同(Protocol)委托给设计院(Model/Tool)。设计院按约定格式交付图纸后,项目经理再根据图纸决定下一步是招标还是买材料。MCP就是把这套人类协作的常识,翻译成AI系统内部的协作协议。

所以,这门课第六讲第三部分的价值,不在于它“发明”了什么,而在于它用一套可复现的结构,把原本散落在各种论文、Demo和工程实践里的隐性共识,显性地拎了出来。它不教你怎么写一个LLM调用函数的代码,而是教你怎么设计一个能让多个LLM、多个工具、多个外部系统,在没有人工干预的情况下,稳定、可追溯、可调试地协同工作的骨架。这才是它区别于其他“Agent实战课”的关键分水岭——前者教你怎么跑通一个Demo,后者教你怎么建一座桥。

提示:如果你在学习中发现讲师把MCP讲得特别“玄乎”,动不动就上升到“下一代AI范式”,那大概率是在用概念包装内容空洞。真正扎实的MCP教学,一定会立刻带你画出Controller的决策流程图,写出Protocol的JSON Schema定义,并让你亲手改写一个工具的输入输出格式来适配它。

2. 拆解MCP三层:不是理论,是每天都要写的三类代码

很多初学者以为MCP是个高大上的架构图,画出来就行。我在带几个模拟项目X的实习生时发现,他们最大的误区,就是把MCP当成一个“最终形态”去设计,而不是当成一个“每日迭代”的工作流。实际上,MCP的三层,对应着开发者每天要写的三类完全不同的代码,它们的开发节奏、测试方式、出错位置都截然不同。下面我用一个真实的简化场景——“自动整理会议纪要并同步到知识库”——来逐层拆解。

2.1 Controller层:Agent的“大脑皮层”,负责思考与决策

Controller是整个系统的指挥中枢,但它不处理任何原始数据。它的输入,是来自Protocol层解析后的结构化指令;它的输出,是发给Protocol层的、同样结构化的下一步行动指令。它唯一的工作,就是“推理+决策”。

在我们的会议纪要场景里,Controller收到的输入可能是:

{ "task_id": "mtg_20240520_001", "raw_transcript": "(一段很长的语音转文字文本)", "user_intent": "提取关键结论、列出待办事项、存入知识库" }

Controller要做的,不是去读那段文字,而是基于预设规则或轻量级模型(比如一个微调过的TinyBERT)判断:“这段文本是否包含明确的待办事项动词(如‘请XX负责’、‘下周提交’)?如果没有,是否需要先调用摘要工具?” 它的输出可能长这样:

{ "next_action": "call_tool", "tool_name": "summarize_text", "tool_input": { "text": "{{raw_transcript}}", "max_length": 300 } }

这里的关键细节是:Controller的代码里绝对不能出现requests.post()或文件读写操作。它只做逻辑判断,所有IO都交给Protocol层。我见过太多人在这里踩坑——为了“快一点”,直接在Controller里调用API,结果导致逻辑和IO混杂,一出错根本分不清是模型理解错了,还是网络超时了。

2.2 Model/Tool层:Agent的“手脚”,负责执行与反馈

这一层才是真正的“干活人”。它可以是:

  • 一个封装好的Python函数(比如def summarize_text(text: str, max_length: int) -> str:);
  • 一个远程API的SDK(比如调用某家文档解析服务的client);
  • 甚至是一个本地运行的CLI工具(比如用pandoc转换格式)。

它的核心契约只有一个:严格遵守Protocol层定义的输入输出格式。这意味着,无论你底层用的是GPT-4还是自己微调的7B模型,只要输入是{"text": "...", "max_length": 300},输出就必须是{"summary": "..."}。这个契约,是Controller能放心调用它的唯一前提。

实操中,我要求实习生在写每一个Tool时,必须先写一个独立的单元测试:

# test_summarize_tool.py def test_summarize_tool(): # 给定一个固定输入 input_data = {"text": "今天开会讨论了Q3市场策略。A同学负责竞品分析,B同学下周提交预算方案。", "max_length": 50} # 调用工具 result = summarize_text(**input_data) # 断言输出必须是dict,且包含"summary"键 assert isinstance(result, dict) assert "summary" in result # 断言摘要长度符合要求 assert len(result["summary"]) <= 55 # 留10%容错

这个测试不关心摘要质量,只关心接口契约。只有契约稳了,上层Controller才能构建可靠的决策链。

2.3 Protocol层:Agent的“通用语言”,负责翻译与校验

Protocol是MCP里最容易被忽视、却最影响长期维护性的部分。它不是简单的JSON序列化,而是一套带校验的通信协议。它要干三件事:

  1. 序列化:把Controller的决策指令,转成Tool能理解的格式;
  2. 反序列化:把Tool的原始返回,转成Controller能理解的格式;
  3. 校验:在每次转换前后,检查数据结构是否符合预设Schema。

我们用Pydantic V2来实现它,因为它的BaseModel天然支持运行时校验和清晰的错误提示。定义一个Protocol Schema:

from pydantic import BaseModel, Field from typing import Optional, Dict, Any class ToolCallRequest(BaseModel): tool_name: str = Field(..., description="要调用的工具名称") tool_input: Dict[str, Any] = Field(..., description="传给工具的参数字典") class ToolCallResponse(BaseModel): success: bool = Field(..., description="调用是否成功") result: Optional[Dict[str, Any]] = Field(None, description="成功时的返回结果") error: Optional[str] = Field(None, description="失败时的错误信息") raw_output: Optional[str] = Field(None, description="原始未解析的输出,用于调试")

Protocol层的代码,就是围绕这两个Model写的转换器:

def serialize_for_tool(controller_output: dict) -> ToolCallRequest: """Controller输出 -> Tool输入""" try: return ToolCallRequest(**controller_output) except Exception as e: raise ValueError(f"Controller输出格式错误,无法序列化为ToolCallRequest: {e}") def deserialize_from_tool(raw_response: Any) -> ToolCallResponse: """Tool原始输出 -> Controller输入""" # 假设Tool返回的是JSON字符串 if isinstance(raw_response, str): try: data = json.loads(raw_response) except json.JSONDecodeError: return ToolCallResponse(success=False, error="非JSON格式响应") else: data = raw_response try: return ToolCallResponse(**data) except Exception as e: return ToolCallResponse( success=False, error=f"响应数据结构错误: {e}", raw_output=str(raw_response) )

这个设计带来的最大好处是:当Tool返回了一个格式错乱的JSON(比如少了个逗号),Protocol层会立刻捕获并返回一个清晰的error字段,Controller就能据此决定是重试、降级,还是直接报错给用户。而不是让错误一路穿透,最后在Controller里抛出一个KeyError: 'summary',让你花两小时去排查到底是模型崩了还是网络断了。

3. 为什么MCP不是银弹:三个必须直面的现实约束

市面上不少课程把MCP描绘成“一招鲜,吃遍天”的万能架构,仿佛只要画出三层图,Agent就能自动变聪明。我在参与某跨平台系统的设计评审时,就亲眼见过一个团队,花了三周时间把MCP三层图做得无比精美,结果第一次集成真实业务API时,全线崩溃。问题不在理念,而在对现实约束的误判。MCP不是魔法,它有自己清晰的边界和代价,忽略它们,只会让项目陷入更深的泥潭。

3.1 约束一:延迟成本是线性叠加的,不是常数

这是最反直觉的一点。很多人以为,把一个单次LLM调用拆成“Controller决策→调用Tool→Tool返回→Controller再决策”,只是多了一次函数调用,开销可以忽略。但真实世界里,每一次跨层调用,都意味着一次不可控的延迟引入。

  • Controller层的轻量模型推理:~100ms(本地CPU)
  • Protocol层的JSON序列化/反序列化:~5ms(可忽略)
  • Tool层的网络请求(调用外部API):~1200ms(平均)
  • Tool层的本地计算(比如PDF解析):~800ms(取决于文件大小)

看起来,一次完整循环是2105ms。但问题在于,一个复杂任务往往需要3-5轮循环。如果第一轮Controller决定“先摘要”,第二轮决定“再提取待办”,第三轮决定“最后存库”,那么总延迟就是2105ms × 3 ≈6.3秒。而用户感知的,不是“三次1秒”,而是“点了按钮,等了6秒才出结果”。这已经远超人类耐心阈值(约2秒)。

我的解决方案从来不是“优化单次”,而是在Controller层内置延迟预估模型。它不预测具体毫秒数,而是给每个Tool打一个“延迟等级”标签:

  • low_latency(<200ms):本地函数、内存数据库查询
  • medium_latency(200ms-2s):稳定云API、轻量计算
  • high_latency(>2s):大文件处理、复杂模型推理

Controller的决策逻辑里,会加入一条硬规则:“如果当前任务链中,已存在一个high_latencyTool,则后续所有决策必须合并,禁止拆分成多轮”。比如,把“摘要+提取待办+生成PPT”三个动作,强行打包成一个Tool的单一调用。这牺牲了一点灵活性,但换来了可接受的用户体验。这不是架构倒退,而是对物理世界规律的尊重。

3.2 约束二:状态管理必须外置,MCP自身不提供

MCP的三层设计,天然倾向于“无状态”。Controller每次收到输入,都当作一个全新请求来处理;Tool每次被调用,都假设输入是完整的。但现实任务充满状态依赖:用户说“把刚才提到的A同学的邮箱发给我”,这里的“刚才”就是一个时间戳状态;或者“对比上个月的报表”,这里的“上个月”是一个上下文状态。

MCP本身不定义、不管理、也不存储任何状态。它只是一个通信协议。这意味着,状态管理必须由你额外设计,并且要小心地“缝合”进MCP的缝隙里。

我们采用的方案是“Context Token + 外部存储”:

  • 每次用户发起新对话,Controller生成一个唯一的context_id(比如UUID);
  • 所有Tool的调用,Protocol层都会自动把这个context_id注入到tool_input里;
  • Tool在执行时,如果需要访问历史,就拿着context_id去查外部Redis缓存(缓存里存着这个对话的所有中间结果);
  • Controller在决策时,也可以主动从Redis里拉取context_id下的最新摘要,作为决策依据。

这个方案的关键在于:所有状态读写操作,都必须发生在Tool层内部,Controller层永远只看到context_id这个字符串。这样,Controller的逻辑依然干净,状态管理的复杂性被隔离在了具体的Tool实现里。我见过最糟糕的做法,是把整个对话历史作为raw_transcript的一部分,每次都传给Controller——这不仅浪费带宽,更让Controller的决策模型变得无比臃肿和不可预测。

3.3 约束三:错误传播路径是双向的,调试必须覆盖全链路

在单体应用里,一个函数报错,你顺着堆栈就能找到源头。但在MCP架构里,错误可以沿着两个方向传播:

  • 正向:Controller决策错误 → Tool执行错误 → 返回错误 → Controller再次错误决策(死循环);
  • 反向:Tool底层依赖(比如数据库连接池耗尽)崩溃 → 返回一个格式错乱的字符串 → Protocol层反序列化失败 → Controller收到一个None→ Controller因None而崩溃。

这就要求你的监控和日志,必须是端到端、带上下文ID的。我们在每个关键节点都强制打点:

  • Controller入口:记录context_id,received_input,decision_time;
  • Protocol序列化后:记录serialized_payload(脱敏);
  • Tool调用前:记录tool_name,context_id,start_time;
  • Tool返回后:记录raw_response,duration_ms,success_flag;
  • Protocol反序列化后:记录deserialized_result(脱敏)。

所有日志行都带上同一个context_id。当用户报告“第3步卡住了”,运维同学只需要在日志系统里搜这个ID,就能瞬间看到从Controller接收到第一个字节,到Tool返回最后一个字节的完整流水。没有这个能力,MCP架构的调试成本会指数级上升。这不是可选项,是上线前的必做项。

4. 从课程Demo到生产落地:一个必须补全的“第四层”

Venkat Reddy的课程,包括绝大多数Agentic AI教学,其Demo都止步于“Controller调用Tool,Tool返回结果,Controller打印成功”。这完美展示了MCP的通信流程,但离真实可用,还隔着一层看不见的“第四层”:Orchestration Layer(编排层)。它不参与核心决策,却决定了整个系统能否在真实环境中活下来。

4.1 编排层的核心职责:兜底、熔断与降级

编排层位于Controller之上,是用户请求和Controller之间的“守门人”。它的存在,是为了应对那些MCP三层本身无法优雅处理的“灰色地带”:

  • 超时熔断:Controller决策本身也可能卡住(比如一个复杂的规则引擎死循环)。编排层必须设置一个全局超时(比如5秒),一旦超过,立即终止整个链路,返回友好的“系统繁忙,请稍后再试”。
  • 限流保护:防止恶意或误用的高频请求把下游Tool(尤其是付费API)打垮。我们用令牌桶算法,在编排层对每个context_id或IP做速率限制。
  • 降级策略:当某个关键Tool(比如知识库同步)持续失败时,编排层可以动态切换逻辑——把“同步知识库”这一步跳过,只做摘要和待办提取,并在返回结果里加一句“知识库同步暂不可用,已保存至草稿箱”。

这个层的代码,通常是一个薄薄的FastAPI中间件:

@app.middleware("http") async def orchestration_middleware(request: Request, call_next): context_id = request.headers.get("X-Context-ID", str(uuid.uuid4())) # 1. 限流检查 if not rate_limiter.is_allowed(context_id): return JSONResponse(status_code=429, content={"error": "Too many requests"}) # 2. 启动全局超时 try: async with asyncio.timeout(5.0): response = await call_next(request) return response except asyncio.TimeoutError: # 3. 超时降级 return JSONResponse( status_code=503, content={ "error": "Service unavailable", "suggestion": "Please try again later", "fallback_available": True } )

4.2 编排层的“隐形”价值:统一可观测性入口

所有MCP三层的日志,最终都要汇聚到编排层进行统一标记和转发。它给每一条日志打上request_id(即HTTP请求ID),并确保这个ID贯穿Controller、Protocol、Tool的每一行日志。这使得SRE团队在排查问题时,不再需要分别登录三台服务器去grep日志,而是在一个地方,输入request_id,就能看到全链路视图。

更重要的是,编排层是唯一能拿到完整HTTP上下文的地方。它知道这次请求来自哪个前端页面、用户的设备类型、甚至HTTP Referer。这些信息,对于分析用户行为、识别恶意爬虫、做AB测试,都至关重要。而MCP的三层,对此一无所知。它们只关心“任务是什么”,不关心“谁在用”。

4.3 为什么课程不讲编排层?一个残酷的真相

因为编排层没有“技术含量”,它全是工程权衡。它不涉及任何前沿论文,不需要你微调模型,也不需要你设计精巧的Prompt。它只关乎:你愿意为一次失败的请求,多花多少毫秒去记录日志?你愿意为防止一次DDoS攻击,多维护几行限流配置?你愿意为一个临时的API故障,多写一个降级分支?

这些事,对追求“快速演示效果”的课程来说,是冗余的;但对任何一个要上线的真实项目来说,是生死线。我带过的团队里,有超过70%的线上P0事故,根源都不是Controller逻辑错了,而是编排层的熔断没配好,或者日志ID没透传,导致故障定位花了6小时。

所以,当你学完这门课的第六讲第三部分,别急着庆祝。真正的功课,是从Controller层往回走,亲手补上这个看似平庸、实则至关重要的第四层。它不会让你的Demo看起来更酷,但会让你的系统,在凌晨三点用户暴增时,依然稳如磐石。

5. 实战复盘:一个MCP项目从0到1的七天攻坚日志

理论讲得再透,不如一次真实的项目复盘。去年,我带着三位实习生,用七天时间,把一个客户提出的“自动处理销售线索邮件”的需求,从零开始,用MCP架构落地。这个过程没有黑科技,只有每天踩坑、填坑、再踩坑的循环。我把每一天的关键动作、遇到的坑、以及最终的解决方案,原原本本记录下来,这就是一份比任何课程都更真实的“MCP生存指南”。

5.1 Day 1:定义边界与拒绝“全功能”

上午,客户发来需求文档,洋洋洒洒五页纸,核心诉求是:“让AI自动阅读销售邮箱里的新邮件,识别高意向客户,提取联系方式,打电话预约,并把通话记录存入CRM。” 这是一个典型的“把AI当神”的需求。

我们的第一反应不是打开IDE,而是召开一场30分钟的“范围砍杀会”。我们拿出白板,把需求拆解成原子任务:

  • ✅ 解析邮件正文(纯文本)
  • ✅ 识别客户公司名(NLP实体识别)
  • ✅ 提取邮箱/电话(正则匹配)
  • ⚠️ 打电话预约(需要集成电话系统,超出当前MVP范围)
  • ❌ 通话记录存CRM(需要客户开放CRM API权限,当前无)

最终,我们和客户确认,Day 1的目标只有一个:输入一封邮件原文,输出一个JSON,包含company_name,contact_email,contact_phone三个字段。其他都是“二期”。这个决策,避免了我们在第一天就陷入电话API的泥潭,把全部精力聚焦在MCP的核心验证上。

注意:永远不要在第一天就承诺“打通所有环节”。MCP的价值,恰恰在于它能让你把一个庞然大物,切成一个个可独立验证的小块。先让一块砖立住,再考虑盖楼。

5.2 Day 2:Protocol Schema的三次重构

我们信心满满地定义了第一个Protocol Schema:

class EmailParseInput(BaseModel): raw_email: str class EmailParseOutput(BaseModel): company_name: str contact_email: str contact_phone: str

结果,下午测试时就崩了。一封测试邮件里,contact_phone字段是空的,Pydantic直接抛ValidationError。我们意识到,Schema不能是“理想状态”,必须是“现实状态”。

第二次重构,我们加上了Optional:

class EmailParseOutput(BaseModel): company_name: str contact_email: Optional[str] = None contact_phone: Optional[str] = None

又崩了。因为有些邮件里,company_name也是模糊的(比如只写了“某科技公司”)。第三次,我们彻底放弃“必填”思维,改为:

class EmailParseOutput(BaseModel): company_name: Optional[str] = None contact_email: Optional[str] = None contact_phone: Optional[str] = None confidence_score: float = Field(0.0, ge=0.0, le=1.0) # 新增置信度

并且,在Tool的实现里,强制要求:哪怕只猜出一个邮箱,也要返回confidence_score=0.3,而不是None。这个改动,让整个链路从“非黑即白”变成了“灰度决策”,Controller可以根据置信度,决定是直接返回,还是标记为“需人工复核”。Schema的每一次重构,都是对现实复杂性的一次低头。

5.3 Day 3:Controller的“懒惰”哲学与首次决策

我们写了第一个Controller,逻辑很简单:

def simple_controller(input_data: dict) -> dict: if "raw_email" not in input_data: return {"error": "Missing raw_email"} return { "next_action": "call_tool", "tool_name": "parse_email", "tool_input": {"raw_email": input_data["raw_email"]} }

运行起来,一切顺利。但到了晚上,实习生A提出一个问题:“如果邮件里有附件PDF,我们是不是应该先调用PDF解析Tool,再把解析后的文本传给parse_email?” 这个问题,暴露了Controller设计的第一个陷阱:过早优化。

我们当时决定,Controller的第一版,必须是“最懒惰”的——它只做一件事:把输入原封不动地转发给第一个Tool。任何“如果...就...”的分支逻辑,都留到第二版。因为第一版的目标,是验证MCP的通信管道是否通畅。加了分支,反而会让问题变得模糊:是管道不通,还是分支逻辑错了?

这个“懒惰”哲学,让我们在Day 3就跑通了端到端,拿到了第一个成功的JSON输出。而那个PDF分支,我们记在TODO里,留到Day 5再处理。

5.4 Day 4:Tool层的“脏数据”洗礼与防御式编程

我们接入了第一个真实邮件数据集(100封),结果成功率只有62%。日志显示,大量失败源于Tool层的“意料之外”:

  • 邮件里有HTML标签<br>,正则匹配邮箱时,把<br>当成了邮箱一部分;
  • 有些邮箱是name[at]domain[dot]com这种防爬写法;
  • 电话号码格式五花八门:+86-138-0013-8000、138 0013 8000、(138) 0013-8000。

我们没有去改Controller,也没有去改Protocol,而是在Tool层内部,加了一层“数据清洗前置”:

def parse_email(raw_email: str) -> dict: # Step 1: 清洗HTML clean_text = re.sub(r'<[^>]+>', ' ', raw_email) # Step 2: 标准化防爬写法 clean_text = clean_text.replace('[at]', '@').replace('[dot]', '.') # Step 3: 提取所有疑似邮箱/电话的字符串 emails = extract_emails(clean_text) phones = extract_phones(clean_text) # ... 后续逻辑

这个教训很深刻:Tool层不是“纯净的数学函数”,它是和真实世界肮脏数据搏斗的第一线。它的代码,必须默认信任“输入是坏的”,并自带消毒能力。Controller和Protocol,永远只看到Tool“清洗后”的干净输出。

5.5 Day 5:引入缓存与性能拐点的出现

随着测试邮件量增加到1000封,平均响应时间从300ms飙升到1.2秒。cProfile分析显示,瓶颈在extract_emails函数,它在每封邮件里都重复执行了相同的正则编译。

我们没有去优化正则,而是在Tool层加了一级LRU缓存:

from functools import lru_cache @lru_cache(maxsize=128) def _compile_email_pattern(): return re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b') def extract_emails(text: str) -> list: pattern = _compile_email_pattern() return pattern.findall(text)

一行@lru_cache,让性能回到了300ms以内。但这引出了一个新问题:缓存是进程内的,而我们的服务是多进程部署的。于是,我们第二天就升级到了Redis缓存,并把lru_cache降级为“本地热点缓存”,形成二级缓存。性能优化,永远是“先加缓存,再看瓶颈”,而不是一上来就啃算法。

5.6 Day 6:编排层上线与第一次熔断

我们把服务部署到测试环境,接入了真实的邮件推送Webhook。不到一小时,监控告警:parse_emailTool的错误率飙升至40%。日志里全是ConnectionResetError。

查原因,是上游邮件服务在高峰期做了限流,我们的请求被随机中断。这时,编排层的熔断器起了作用:它检测到连续5次失败,自动触发半开状态,将后续请求暂时路由到一个“静态Fallback Tool”,它只返回{"error": "邮件服务暂时繁忙", "suggestion": "请稍后重试"}。

这个“失败”,反而成了我们最有价值的一次验证。它证明,编排层不是摆设,而是真正的安全网。我们当天就调整了熔断阈值,并增加了对上游服务健康度的主动探测。

5.7 Day 7:交付、文档与那个没写的“重试按钮”

第七天,我们交付了可运行的服务、完整的API文档、以及一份《MCP架构决策记录》,里面详细说明了为什么选择Pydantic而不是Marshmallow,为什么confidence_score的范围是0-1而不是0-100,为什么缓存用了Redis而不是Memcached。

但客户在验收时,指着UI界面上那个“重试”按钮问:“如果解析失败了,能不能点一下就重试?” 我们愣住了。因为在MCP设计里,“重试”不是一个按钮,而是一个需要Controller重新评估、Protocol重新序列化、Tool重新执行的完整链路。我们当时没预留这个能力。

这个“没写的按钮”,成了我们最大的收获。它提醒我们:MCP再优雅,最终也要服务于人。那天下午,我们给Controller加了一个retry_count字段,让它在决策时,能根据失败次数,选择不同的降级策略。那个按钮,终于有了灵魂。

这七天,没有惊天动地的创新,只有无数个微小的、务实的、甚至有点笨拙的决定。而正是这些决定,把一个课程里的概念,变成了一个能为客户创造真实价值的系统。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进