学员问我:审计日志到底由 Python 还是 Go 落库?我为什么只给了“有条件通过“
在一次晚八点的 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 执行异常 |
其中,50005 和 50006 是这次需要补充的错误语义。
例如:
- Go 内部工具调用超时、连接失败或返回异常状态码,记录为
50005; - Skill 代码抛错、规则执行异常或输出不符合契约,记录为
50006; - MySQL 或 Repository 发生真实读写故障,才使用
50004; - 模型服务超时或响应无法解析,使用
50003。
错误码不是为了让接口字段显得丰富。
它真正服务的是三件事:
- 自动化测试;
- 故障定位;
- 后续的降级与重试策略。
第四个问题:脱敏不能只处理 JSON 字段
审计事件中会包含输入摘要、输出摘要和错误信息。
很多系统只对 token、password、authorization 等结构化字段做脱敏,却忽略了普通字符串。
但手机号、身份证号、邮箱、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_id和error_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 是否完成最终脱敏和持久化;
50005和50006是否符合实际故障;- 一次客服咨询能否按
trace_id还原执行过程。
以后,我也会继续记录 LexAgent 项目中的这些真实故事:
学员提出真实问题,我通过追问识别问题本质,给出架构判断和执行建议,最后再由代码和测试验证答案。
这些故事可能发生在需求评审会、项目答疑会,也可能发生在每天晚上的项目总结会上。
它们不会只有漂亮的页面和架构图,还会包括争议、取舍、错误、测试和暂时没有完成的部分。
因为真实的技术成长,从来不是"老师把答案告诉你"。
而是你先带着方案进入项目,然后在一次次评审和验证中,逐渐学会自己作出工程判断。
更多推荐


所有评论(0)