--- title: 从一次 tenantId 联调 bug,看我们该怎么给 AI 项目补齐 harness author: Gamehu date: 2026-03-07 20:30:00 tags: - AI Coding - Harness - 多租户 - 工程化 categories: - AI ---
AI 工程化
前几天我读了 OpenAI 那篇文章:[Harness engineering: leveraging Codex in an agent-first world](https://openai.com/index/harness-engineering/)。 我读完最大的感受不是“AI 又强了”,而是另一个更接地气的结论: 很多时候,不是模型不够强,而是你的工程环境没有把“正确性”暴露给模型。 这两天我正好在自己项目里处理一个非常典型的问题:运营端新增门店和校验门店名时,如果和其它租户重名会误报;员工后台接口也有类似的租户范围问题。表面上看只是个 `tenantId` bug,但整个过程把一个事实暴露得很彻底: **AI 想高效干活,前提不是 prompt 更长,而是 harness 更清楚。** --- ## 一、这次 bug 表面是租户问题,实质是“环境表达不清” 最初的现象其实很迷惑: 1. 运营端调用 `admin/store/checkName` 2. 明明传了 `tenantId` 3. 但接口判重却像是按别的租户在查 如果只看 Controller 参数,你会以为“不是都传进来了吗”。 但真正往下追,就会发现系统里其实混着两套租户来源: - 请求里显式传入的 `tenantId` - 线程上下文里的 `TenantContextHolder` 理论上,`TenantContextHolder` 也不是错的。因为网关或 filter 确实会从 header 里解析租户并写入上下文。问题在于: **后台管理接口的业务语义,不是‘当前会话属于哪个租户’,而是‘当前运营账号要操作哪个目标租户的数据’。** 这两个概念,在多租户后台系统里根本不是一回事。 所以这次真正的问题不是一句“代码写错了”,而是: 系统没有把“哪一个租户才是业务真相”表达得足够显式。 这就是 harness 问题。 --- ## 二、OpenAI 那篇文章对我最有启发的,不是模型能力,而是“把环境当产品做” 那篇文章里有几层意思我特别认同,我这里不逐字复述,只说我自己的理解。 ### 1)Agent 的上限,很大程度取决于它能不能直接看到真实运行环境 如果模型只能读代码,看不到: - 实际 HTTP 请求长什么样 - token 从哪里来 - 当前服务到底连的是哪套数据库 - 逻辑删除字段的业务语义是什么 那它就很容易陷入一种“静态推理正确,动态结论错误”的状态。 这次我们项目里就连续遇到了: - 代码已经改了,但运行中的服务没重启 - `checkName` 返回 `true/false` 的语义,调用方理解反了 - 数据库里能查到门店记录,但那条记录其实已经逻辑删除 这些都不是大算法问题,而是**工程上下文没有被收束成一个可验证的工作台**。 ### 2)入口文档不该是经验大杂烩,而应该是导航页 以前很多项目的 `AGENTS.md`、模块说明文档,最后都会越写越长。 每次踩坑补一条,最后谁都不想看,AI 也很难稳定执行。 所以我这次顺手把项目里的规则做了一个调整: - `AGENTS.md` 和 `CLAUDE.md` 保留高层原则 - 具体的联调、token、租户、契约规则下沉到 `docs/testing/` 这不是为了“文档好看”,而是为了让规则能被持续修正,而不是散落在三个地方互相漂移。 {% asset_img 1.jpg %} ### 3)真正有价值的,不是“写了规则”,而是“规则能被验证” 如果规则只是: - 显式传 `tenantId` - 统一走网关 - 做真实 HTTP 测试 那它还是有点抽象。 真正有效的规则必须长成下面这样: - 运营端接口测试时,直接向用户索取运营端 token - 同一个 token,对照请求 `tenantId=A/B` - 至少保留一条原始 HTTP 响应 - 必要时补查数据库解释“为什么接口返回这样” 一旦规则写到这个粒度,AI 和人都更难自欺欺人。 --- ## 三、这次我把项目规则怎么改了 结合这次联调前后对比,我最后把项目里的规则改成了三层。 ### 第一层:入口文档只讲方向,不再堆细则 我在项目根目录 `AGENTS.md` 和 `goodsop-app-server/CLAUDE.md` 里增加了一个明确入口: - `docs/testing/README.md` - `docs/testing/admin-http-harness.md` - `docs/testing/tenant-data-scope.md` 意思很简单: 入口文档只负责告诉 AI“去哪儿看”,真正经常变化的实战规则集中维护。 ### 第二层:把运营端 HTTP harness 写成可执行规则 这次新增的 `admin-http-harness.md` 里,我重点固化了几件事: 1. 所有运营端联调统一走 `http://localhost:9999` 2. 测试运营端接口时,必须直接问用户要 token 3. 同一轮验证里,优先用同一个 token 只切换 query `tenantId` 4. 每次改动至少保留 1 个真实业务接口的原始响应 5. 失败排查顺序固定为:连通性 -> 网关 -> Nacos -> 鉴权上下文 -> 业务代码/SQL 这几条看起来朴素,但非常关键。因为 agent 一旦没有这些硬边界,就会在“如何拿 token”“是不是该直连服务”“这个请求到底算不算验证”上浪费很多轮次。 ### 第三层:把租户数据范围语义写明白 `tenant-data-scope.md` 里,我把这次最核心的结论直接写死了: 1. 后台管理接口的数据范围以显式传入的目标 `tenantId` 为准 2. `TenantContextHolder` 是上下文机制,不是后台业务真相 3. `checkName/checkPhone` 必须同时校验接口契约和逻辑删除语义 尤其是第三点,这次特别有代表性。 我们一开始看到有些门店名“库里明明有记录,接口却返回可用”,很容易怀疑代码没改对。 后来一查数据库才发现:那几条是逻辑删除记录。也就是说,**数据库里有行,不等于业务上仍占用名称**。 这个结论如果不被写进规则里,下次还会重复争论。 {% asset_img 2.jpg %} --- ## 四、这次代码层面的前后对比,也很说明问题 如果只看代码,这次改动其实不算复杂,核心就是两件事。 ### 改动前 - Controller 收到了 `tenantId` - 但部分 service 逻辑还是依赖 `TenantContextHolder` - `checkName` 的返回在失败时没有稳定给出 `data=false` - 联调时容易把“请求头租户”“目标业务租户”“逻辑删除记录”混在一起 ### 改动后 - `admin/store/*`、`admin/consultant/*` 显式把 `tenantId` 往下传 - 需要上下文的地方用 `TenantBroker.applyAs(tenantId, ...)` - `checkName` 改成: - 可用:`data=true` - 不可用:`data=false` - 真实接口回归不只看代码,还做了: - `tenantId=5584` 与 `tenantId=1` 对照请求 - 门店新增、关店、员工新增、修改、离职 - 必要时补查 PG 解释结果 这里最关键的,不是“把某个 if 改对了”,而是从“我觉得这样应该对”变成了“我能证明它对,而且能解释为什么”。 这就是 harness 的价值。 {% asset_img 3.jpg %} --- ## 五、对我们这种业务项目来说,AI 真正缺的不是智商,而是工作台 很多人谈 AI Coding,喜欢把重点放在模型选择、prompt 技巧、上下文窗口大小。 这些当然重要,但我现在越来越觉得,对真实业务项目来说,下面这些东西更值钱: ### 1)可直接使用的环境入口 - 正确的网关地址 - 正确的 token 获取方式 - 正确的数据库连接信息 - 正确的日志和服务发现排查路径 ### 2)可复用的验证脚本或验证模板 不是“你自己去测一下”,而是给出: - 请求地址 - header - body - 对照 tenantId - 预期差异 ### 3)不会漂移的规则系统 很多团队的问题不是没有规则,而是规则散在聊天记录、群公告、项目文档、某个人脑子里。 一旦 AI 进场,这种问题会被放大得更厉害。 因为 AI 特别依赖“哪个文档才是 system of record”。 --- ## 六、我准备继续往前做的,不只是写博客 这次规则改完以后,我更想补的是一套更完整的 harness,而不只是几段说明文档。 如果继续做,我下一步大概率会补这些东西: 1. `scripts/verify-admin-store.sh` - 自动完成 `page/checkName/create/close` 2. `scripts/verify-admin-consultant.sh` - 自动完成 `page/checkPhone/create/update/resign` 3. 运行态环境说明 - 当前服务实际连接哪套 PG/Redis 4. 接口契约回归样例 - 尤其是 `data/code/msg` 这种容易被误解的接口 因为写到最后我越来越确信一件事: AI 工程化的竞争力,不是“谁的模型更像天才”,而是“谁先把自己的真实环境整理成一个不会误导 agent 的工作台”。 --- ## 结语 OpenAI 那篇文章给我的最大提醒,是别把 agent 当成一个只会补代码的聊天机器人。 它更像一个能力很强、速度很快,但极度依赖环境质量的工程协作者。 如果你的环境是模糊的: - 文档入口混乱 - token 获取方式混乱 - 租户语义混乱 - 接口契约混乱 那 AI 就会在这些噪音里反复打转。 但如果你把 harness 补起来,很多原来需要人肉盯着的事情,就会突然顺很多。 所以这次一个看似普通的 `tenantId` bug,最后给我的启发反而比 bug 本身更大: **以后优化 AI Coding,不只是继续追模型,也要继续做环境。**