第 36 篇
工程技术
MCP 调用为什么需要分阶段执行轨迹
结合实际开发项目,讨论MCP 调用为什么需要分阶段执行轨迹,说明设计理由、关键源码、验证方法及适用边界。
本文目录 · 8 节
本文属于 Memory Center:AI 编程上下文与记忆系统工程实践,第 36 篇,位于“验证与完整实践”模块。案例背景统一采用实际开发项目;复现步骤使用脱敏教学样例,不代表特定生产事故。
从实际问题出发
一次工具调用用了很久,单条总耗时无法说明是在授权、检索、模型请求还是结果装配阶段等待。分阶段轨迹使维护者能从“慢了”继续追问“慢在哪里”。
度量与正文留存分开
轨迹可以记录工具名、阶段、开始时间、耗时和受控错误码,而不保存请求和响应正文。这样降低留存敏感载荷的风险,但也意味着排错时不能指望轨迹还原完整业务内容。

图中展示的是本篇关注的职责和顺序。正常流程之外,还要结合下面的源码与实验检查失败分支;它不是对某次线上调用的运行时追踪。
取消与异常同样需要计量
阶段计时应在 finally 路径结束,即使异常或取消也保留耗时和状态。持续时间适合使用单调时钟,时间戳用于展示顺序;两者用途不同。分阶段时间还要区分嵌套,不能机械相加。
为什么阶段耗时不能直接相加
如果“检索”阶段内部又记录了“向量请求”阶段,两段时间存在包含关系。把父阶段与子阶段相加会重复计时。观察瀑布图时应先看总耗时,再定位关键路径,而不是把每个 span 都当成互相独立的成本。
耗时采用单调时钟,展示时间采用墙上时钟;系统时间调整不应让一次持续时间变成负数。记录错误码也要有长度和内容边界,不能顺手把原始异常正文作为永久可搜索数据。
关键源码与阅读方法
源码基线为 Memory Center 6e90553(文档标注 5.0.0b24)。定位:memory_center/platform/call_traces.py,函数 span,起始行 33。
以下为当前基线中的函数代码;依赖项目模块,不能作为独立脚本执行。
def span(name):
trace=current_trace.get()
if trace is None:
yield
return
started=time.monotonic();row={'name':name,'started_at':now(),'status':'running'}
trace['spans'].append(row)
try:
yield
except BaseException as exc:
row['status']='cancelled' if type(exc).__name__=='CancelledError' else 'failed'
row['error_code']='CALL_CANCELLED' if row['status']=='cancelled' else error_code(exc)
raise
else:
row['status']='succeeded'
finally:
row['duration_ms']=round((time.monotonic()-started)*1000,3)
row['finished_at']=now()
阅读时先找输入由谁提供,再区分纯计算、读取和写入;遇到异常或提前返回,追问调用方会得到什么,以及已有状态是否仍然有效。上面的代码只证明展示范围内的实现,跨模块行为应继续查看调用处和相应测试。
用一个小实验核对设计
建议在独立临时目录和隔离数据中操作,使用虚构的接口、文档与项目名称。
- 对正常、失败和取消路径分别执行受控调用。
- 检查阶段终态。
- 比较总耗时与嵌套阶段。
**要核对的结果:**轨迹中不应含完整查询正文,失败调用也必须有明确状态和可定位错误码。
可把观察记录为下表;没有执行的项写“未执行”,不能用设计预期冒充实测结果。
| 核对项 | 应保留的证据 |
|---|---|
| 使用条件 | 源码版本、配置、输入与项目范围 |
| 正常行为 | 实际返回、来源位置或状态变化 |
| 关键失败分支 | 错误码、可见结果与原状态是否保留 |
| 结论范围 | 已覆盖、未覆盖和需要进一步验证的条件 |
适用边界与继续阅读
本篇以静态源码与项目设计为依据,给出可执行的核对方法,未据此宣称已经完成新的性能或生产环境测试。不同版本的入口、默认值和底层实现可能变化,应先核对当前合同,再把方法用于自己的实际开发项目。
理解这一节后,可以回到专题总导航,沿同一模块继续阅读;顺序学习时使用专栏内的上一篇和下一篇。涉及具体技术选择,应连同本篇的代价与限制一起采用。