在一次晚八点的 LexAgent 项目需求评审会上,学员小鹿带来了一份方案。

她认领的是 M-04:MCP 审计持久化。

在正式评审之前,她已经提交了一份《002 M-04 功能细节确认》,并给出了一条看起来比较清楚的技术链路:

Python MCP
  → POST /internal/audits
  → Go 内部接口
  → MySQL audit_log

她在会上提出的问题,可以概括为:

Python MCP 最清楚每次工具和 Skill 调用了什么。如果让 Python 产生审计事件,再调用 Go 的内部接口写入 MySQL,这条链路是否合理?重试、异常和上下文信息应该怎么处理?

这是一个好问题。

而且我能看出来,她不是等着我直接给答案,而是已经先理解了现有代码,再带着方案来参加评审。

Python MCP 已经可以在工具或 Skill 调用结束时产生审计事件;Go 侧也已经有统一的 AuditLog Model、Repository 和 Harness 审计写入能力。

从大的方向看,她的判断是对的。

但我最终给出的评审结论不是"通过",而是:

有条件通过。总体方向正确,按评审结果冻结契约后,可以进入编码。

为什么不是直接通过?

因为一个技术方案"方向合理",和它"可以直接编码",中间还隔着一段很长的路。

我没有先回答"选 Python 还是 Go"

评审开始后,我没有马上告诉她应该由哪一侧负责落库,而是先反问了一个问题:

你希望这条审计记录最终证明什么?

如果目标只是证明"Agent 调用过某个工具",那么 Python 打一条日志也许就够了。

但如果目标是:几分钟以后,有人拿着一条法律咨询来问我们:

  • 这次回答依据了什么?
  • 进入了哪个 Agent 节点?
  • 调用了哪个 MCP 工具?
  • 这次 Agent 有没有发生重试?
  • 为什么系统生成了这条跟进任务?
  • 如果回答出现问题,应该去 Go、Python、RAG 还是 MCP 排查?

那就不是一行日志能够解决的问题了。

LexAgent 的一次客服咨询,可能经过这样一条链路:

用户发起咨询
  ↓
Go Gin 接收业务请求
  ↓
身份与权限检查
  ↓
Python FastAPI Agent
  ↓
LangGraph 节点编排
  ↓
RAG 检索
  ↓
MCP 工具或 Skill 调用
  ↓
Go 业务接口
  ↓
MySQL 写入会话和跟进数据

这里涉及多个服务、多个节点和多次调用。

每一层都可以打印日志,但如果缺少统一的上下文,最后就会出现一种非常典型的问题:

Go 有 Go 的日志,Python 有 Python 的日志,MCP 也记录了工具调用,但没有人能够确定这些记录是不是来自同一次 Agent 执行。

所以,在讨论谁来落库之前,必须先明确审计要还原什么。

我让学员先把几个 ID 分清楚

我在评审会上把几个容易混淆的概念拆开了:

trace_id    一次外部业务请求
run_id      一次 Agent 执行
session_id  一段业务会话
event_id    一条独立的审计事件

另外还有一个重要字段:

caller      发起这次调用的节点或服务

为什么不能只保留一个 ID?

假设客户在同一个客服会话里连续追问三次:

session_id 相同
trace_id 不同
run_id 不同

而在其中一次回答中,Agent 又连续调用了知识检索、客户查询和跟进任务三个工具:

trace_id 相同
run_id 相同
event_id 不同
caller 不同

如果没有把这些概念区分清楚,数据库里即使存了很多审计记录,也很难还原真实的执行过程。

我告诉小鹿:

这不是多加几个字段的问题,而是要先建立一套跨 Go、Python、LangGraph 和 MCP 的运行上下文契约。

这也是我没有让她马上进入编码的第一个原因。

主链路正确,但编码前还差四个关键边界

我没有推翻她的方案。

最终仍然保留:

Python MCP
  → POST /internal/audits
  → Go Internal API
  → MySQL audit_log

理由很清楚:

  • Python MCP 最了解工具和 Skill 的调用过程;
  • Go 负责稳定的业务边界和核心数据;
  • MySQL 是当前统一的数据持久化位置;
  • Python 不应该绕过 Go 直接写业务数据库;
  • 审计数据入库前,需要由 Go 做最终校验和脱敏。

但是,我要求她在编码前先冻结四个问题。

第一个问题:HTTP 重试会不会写出两条审计记录?

我给她举了一个场景。

Python 把审计事件发给 Go。Go 已经成功写入 MySQL,但在返回响应之前,网络发生了超时。

Python 没有收到成功响应,于是再次发送。

如果没有幂等设计,数据库里就会出现两条完全相同的审计记录。

这种问题在正常演示中很难暴露,因为只有发生超时和重试时才会出现。但一旦进入真实业务,重试是不能回避的。

所以,我要求在 audit_log 中增加:

event_id
run_id
error_code

其中,event_id 作为跨进程审计事件的幂等键,并建立唯一约束。

同一个 event_id 第一次提交时正常写入;再次提交时应该被识别为重复事件,并作为成功处理,而不是返回服务器错误。

这个设计的目的不是让接口看起来更复杂,而是确保:

一次工具调用,无论发送端重试多少次,最终只能产生一条审计记录。

第二个问题:审计服务故障,能不能让客服咨询失败?

我的答案很明确:不能。

审计很重要,但它不能反过来绑架主业务。

如果 MCP 每次调用结束后,都同步等待 Go 写完 MySQL,那么审计接口一旦超时,原本正常的客服咨询也可能跟着失败。

但我们也不能简单地启动一个无限制的后台任务,因为审计服务持续不可用时,内存中的任务可能不断堆积。

最终,我给出的方案是使用一个有容量上限的进程内队列:

MCP 调用结束
  → 生成 event_id
  → 放入有界内存队列
  → 立即返回原有 MCP 结果

后台发送 Worker
  → POST /internal/audits
  → 网络异常、超时、5xx 最多重试一次
  → 400、401 不重试
  → 最终失败只记录脱敏告警

这里我冻结了几个边界:

  • 队列必须有最大容量;
  • 不能无限重试;
  • 重试必须沿用相同的 event_id
  • 审计失败不得改变 MCP 原有业务结果;
  • 审计告警本身也不能包含敏感信息。

Python 负责让审计发送不阻塞主业务。

Go 则负责同步完成数据库写入。只要 POST /internal/audits 返回成功,就应该意味着事件已经真正持久化,而不是仅仅"收到请求"。

两边的职责不能混在一起。

第三个问题:错误码不能只写一个"调用失败"

评审过程中,我们还发现现有实现存在错误语义混用。

内部 HTTP 请求超时,有时会被归类为数据库异常;Skill 代码执行异常,也可能被归到 LLM 调用失败。

如果所有异常最终都变成"调用失败",开发者排查时仍然只能从头翻日志。

因此,我重新冻结了错误码的含义:

错误码 含义
40001 参数或 Schema 校验失败
50002 工具调用被拒绝
50003 LLM 调用失败
50004 数据库异常
50005 内部依赖调用失败
50006 Skill 执行异常

其中,5000550006 是这次需要补充的错误语义。

例如:

  • Go 内部工具调用超时、连接失败或返回异常状态码,记录为 50005
  • Skill 代码抛错、规则执行异常或输出不符合契约,记录为 50006
  • MySQL 或 Repository 发生真实读写故障,才使用 50004
  • 模型服务超时或响应无法解析,使用 50003

错误码不是为了让接口字段显得丰富。

它真正服务的是三件事:

  • 自动化测试;
  • 故障定位;
  • 后续的降级与重试策略。

第四个问题:脱敏不能只处理 JSON 字段

审计事件中会包含输入摘要、输出摘要和错误信息。

很多系统只对 tokenpasswordauthorization 等结构化字段做脱敏,却忽略了普通字符串。

但手机号、身份证号、邮箱、Bearer Token 和 API Key 完全可能出现在 error_msg 或文本摘要中。

因此,我要求采用两层脱敏:

Python 第一层脱敏
  ↓
避免明文敏感信息离开 Python 进程
  ↓
Go 入库前再次脱敏
  ↓
保证数据库不保存明文敏感信息

Go 是最终责任层。

同时,我又指出了一个容易被忽视的问题:当前 Go 侧有一处字符串截断按 UTF-8 字节处理。

如果正好截在一个中文字符中间,最终保存的就可能是非法 UTF-8。

这个问题在英文测试里未必出现,但法律咨询大量使用中文,所以必须改成按 Unicode 字符截断。

这就是为什么我一直强调:

企业项目里的问题,往往不在宏大的架构图上,而在一个字段、一种重试和一次中文字符串截断里。

方案拍板后,我给的不是"开工吧"

经过评审,我们冻结了内部接口:

POST /internal/audits
Content-Type: application/json
X-Internal-Token: <internal-token>
X-Trace-Id: <trace-id>

它只能用于 Python 与 Go 之间的内部通信:

  • 使用独立的 X-Internal-Token
  • 不使用公共 API Key;
  • 不挂载在 /api/v1
  • 不经过浏览器 RBAC;
  • 不配置浏览器 CORS;
  • 外部 Nginx 和网关不得暴露 /internal/*

这里的"不经过浏览器 RBAC",不等于没有鉴权。

浏览器和服务间调用本来就是两种不同的信任边界,不能为了复用中间件把它们混为一谈。

审计事件需要携带:

{
  "event_id": "event-001",
  "trace_id": "trace-001",
  "run_id": "run-001",
  "session_id": "session-001",
  "caller": "agent.nodes.answer",
  "tool_name": "legal_qa_skill",
  "step_type": "skill_call",
  "success": true,
  "error_code": null,
  "latency_ms": 83
}

完成这些决策之后,我仍然没有只对小鹿说一句"可以开始写了"。

我给她的是一份验收清单。

我要求代码最终回答这些问题

Go 侧至少要证明:

  • 错误的内部 Token 会被拒绝;
  • 缺失字段和非法 step_type 会被拒绝;
  • 正常事件能够落库;
  • 相同 event_id 提交两次,数据库只产生一行;
  • run_iderror_code 可以写入并查询;
  • 手机号、身份证号、Token 和密码不会以明文入库;
  • 中文摘要截断后仍然是合法 UTF-8;
  • 数据库异常使用正确错误码。

Python 侧至少要证明:

  • 审计事件包含完整上下文;
  • trace_id/run_id/session_id/caller 在节点调用中不会丢失;
  • 网络、数据库、LLM 和 Skill 异常不会混用错误码;
  • 网络异常和 5xx 最多重试一次;
  • 重试沿用相同 event_id
  • 队列满或审计发送失败,不会改变原 MCP 业务响应;
  • 本地告警不会泄露敏感信息。

最后,还要执行一条集成验收链路:

发起一次客服咨询
  ↓
产生 trace_id/run_id/session_id
  ↓
Agent 执行 Skill 和 MCP Tool
  ↓
产生审计事件
  ↓
Go 完成持久化
  ↓
按 trace_id 查询审计记录
  ↓
还原同一 Trace 下的节点、状态、错误码和耗时

同时注入三类故障:

  • Skill 主动抛错,应记录 50006
  • 内部工具超时,应记录 50005
  • 审计接口不可用,客服业务结果不能发生改变。

做到这里,才能说方案通过了代码和测试验证。

专业判断也包括:这次坚决不做什么

在项目评审中,还有一个很容易出现的问题:任务不断膨胀。

既然已经讨论审计,有人很自然会想到:

  • 要不要顺便接 OpenTelemetry?
  • 要不要接 LangFuse?
  • 要不要引入 Kafka?
  • 要不要补一个审计管理后台?
  • 要不要增加多租户隔离?
  • 要不要把 Token 和成本统计也做了?
  • 要不要实现业务与审计的分布式事务?

这些方向以后可能都有价值。

但我给小鹿的决定是:这次不做。

M-04 本轮只解决 Python MCP 审计事件的持久化、幂等、非阻塞、错误语义、上下文传递和脱敏。

如果把所有相关能力都塞进来,这个任务就不再是审计持久化,而会变成重新设计整个可观测平台。

工程能力不只是知道什么值得做,也包括知道:

什么现在不能做,为什么不能做,以及什么时候再做。

为什么我坚持让学员先提交方案

有人可能会问:既然这些问题我已经知道,为什么不直接把最终方案发给学员,让她照着写?

因为那样只能得到代码,不一定能得到能力。

让学员先阅读代码、提出问题并提交方案,我才能看出来她真正理解了什么,又在哪个地方只看到了主链路,没有看到工程边界。

小鹿的原方案并不是错误方案。

她已经找到了正确方向,缺少的是:

  • 重试幂等;
  • 非阻塞降级;
  • 错误码语义;
  • 跨服务上下文;
  • 双层脱敏;
  • 可执行的验收标准。

需求评审不是为了证明学员哪里做错了。

我的工作,是把她提交的"方向合理"继续推进成:

职责边界清楚
  ↓
接口契约冻结
  ↓
代码可以实现
  ↓
测试可以验证
  ↓
结果可以验收

这也是我们在 LexAgent 项目中坚持开需求评审会、项目答疑会和项目总结会的原因。

我不希望学员只是拿到一套完整代码,然后在面试时背诵项目亮点。

我更希望他能够说清楚:

  • 当时遇到了什么问题;
  • 自己最初提出了什么方案;
  • 评审中发现了哪些边界;
  • 最终为什么选择这个设计;
  • 如何通过测试证明方案成立;
  • 哪些能力被明确留到下一阶段。

只有这样,一次开发任务才会真正转化成个人技术能力。

这次评审留下的,不只是"有条件通过"

M-04 下一步会进入编码和验收。

我们会继续验证:

  • 四类上下文是否完整贯穿;
  • event_id 是否真正保证幂等;
  • Python 审计异常是否被隔离;
  • Go 是否完成最终脱敏和持久化;
  • 5000550006 是否符合实际故障;
  • 一次客服咨询能否按 trace_id 还原执行过程。

以后,我也会继续记录 LexAgent 项目中的这些真实故事:

学员提出真实问题,我通过追问识别问题本质,给出架构判断和执行建议,最后再由代码和测试验证答案。

这些故事可能发生在需求评审会、项目答疑会,也可能发生在每天晚上的项目总结会上。

它们不会只有漂亮的页面和架构图,还会包括争议、取舍、错误、测试和暂时没有完成的部分。

因为真实的技术成长,从来不是"老师把答案告诉你"。

而是你先带着方案进入项目,然后在一次次评审和验证中,逐渐学会自己作出工程判断。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐