Day 07 / 共 20 天 · 第 2 周 分层与契约
IDL 契约入门
这几天反复出现的"Thrift 接口""生成代码禁止手改"到底是怎么回事?今天打开 idl/thrift/,看这份契约文件是怎么"一处定义,前后端通吃"的。
📍 你在整门课的位置 · 第 2 周 分层与契约(共 4 周 · 20 天)
D6 DDD 与 Wire→
D7 IDL 契约→
D8 Foundation→
D9 Prompt 读路径→
D10 调试与 LLM
L01
为什么要"契约先行"
🤔 痛点:前端说接口字段是 promptId,后端敲成了 prompt_id,联调地狱由此开始
如果前后端各自手写接口文档、各自手写请求/响应结构体,字段名拼错、类型不一致、少传了一个必填字段,这些坑几乎每个多人协作项目都踩过。
💡 本质:一份 .thrift 文件是"唯一真相源",Go 和 TypeScript 类型都从它生成
Coze Loop 把所有接口的请求/响应结构、字段类型、路由路径,全部写在 idl/thrift/ 下的
.thrift 文件里。改接口只改这一份文件,然后跑生成脚本,前后端的类型定义会自动同步更新、不可能出现"前后端字段不一致"的问题——因为它们本来就是同一份定义生成出来的。🍼 类比:合同的正本与副本
.thrift 文件就像一份合同的"正本",Go 的 kitex_gen 和 TypeScript 的类型定义都是从正本盖章复印出来的"副本"——副本内容永远和正本一致,你也不会去手改副本(改了也没用,下次重新复印又会被覆盖)。L02
idl/thrift 目录地图
docs/guidance/idl-codegen-guide.md 给出的目录结构,和 Day 01 的六大模块严格对应:
idl/thrift/
├── base.thrift # 公共基础类型(分页、通用响应等)
├── extra.thrift # 扩展类型
├── trajectory.thrift # 轨迹类型
└── coze/loop/
├── apis/ # 顶层 API 接口聚合
├── data/ # data 模块类型
├── evaluation/ # evaluation 模块类型
├── foundation/ # foundation 模块类型
├── llm/ # llm 模块类型
├── observability/ # observability 模块类型
└── prompt/ # prompt 模块类型
├── coze.loop.prompt.manage.thrift # 增删改查 Prompt 的 service 定义
├── coze.loop.prompt.debug.thrift # 调试相关
├── coze.loop.prompt.execute.thrift # 执行相关
├── coze.loop.prompt.openapi.thrift # 开放 API
├── coze.loop.prompt.tool_manage.thrift
└── domain/prompt.thrift # Prompt 领域数据结构定义
命名规律:每个
.thrift 文件对应一个 Kitex Service(一组相关接口),domain/ 子目录下放的是纯数据结构(不含接口方法),被多个 service 文件 include 复用——这和 Go 里"domain/entity 定义数据、application 定义用例"的思路是相通的。L03
一个 thrift service 长什么样
idl/thrift/coze/loop/prompt/coze.loop.prompt.manage.thrift 开头几行,就是 Day 04/05 一直在用的 CreatePrompt 接口的"正本":
namespace go coze.loop.prompt.manage
include "../../../base.thrift"
include "./domain/prompt.thrift"
service PromptManageService {
// 增
CreatePromptResponse CreatePrompt(1: CreatePromptRequest request)
(api.post = '/api/prompt/v1/prompts')
ClonePromptResponse ClonePrompt(1: ClonePromptRequest request)
(api.post = '/api/prompt/v1/prompts/:prompt_id/clone')
// 删
DeletePromptResponse DeletePrompt(1: DeletePromptRequest request)
(api.delete = '/api/prompt/v1/prompts/:prompt_id')
// 查
GetPromptResponse GetPrompt(1: GetPromptRequest request)
(api.get = '/api/prompt/v1/prompts/:prompt_id')
}
struct CreatePromptRequest {
1: optional i64 workspace_id (api.js_conv='true', vt.not_nil='true', vt.gt='0', go.tag='json:"workspace_id"')
// ...
}
💡 本质:注解(annotation)里藏着路由、校验规则和跨语言细节
Day 04 看到的
/api/prompt/v1/prompts 路由,源头就是这里的 (api.post = '...') 注解——Hertz 生成器解析这些注解自动生成 router_gen.go。vt.not_nil='true'、vt.gt='0' 是校验规则,对应 Day 05 c.BindAndValidate 时生效的那些约束。go.tag='json:"workspace_id"' 直接控制生成出来的 Go struct 字段带什么 JSON tag。api.js_conv='true' 是处理 JS 里大整数精度丢失问题的特殊转换标记。📌 小技巧:以后遇到不确定某个接口的路由或字段校验规则,直接去对应的
.thrift 文件里搜方法名,比在生成代码的海洋里搜要快得多,而且这里才是"意图"的原始表达。L04
kitex_gen vs loop_gen:两种生成产物
🤔 痛点:backend 下有 kitex_gen 和 loop_gen 两个生成目录,傻傻分不清
这两个目录都是"禁止手改",但职责完全不同,Day 05 已经用过它们,今天讲清楚区别。
| 目录 | 内容 | 典型文件 |
|---|---|---|
kitex_gen/ | Thrift 原生生成的类型定义 + Service 接口(标准 Kitex 产物) | kitex_gen/.../manage/coze.loop.prompt.manage.go(CreatePromptRequest 结构体、PromptManageService 接口) |
loop_gen/ | 在 kitex_gen 基础上,额外生成的"本地调用客户端"(Coze Loop 自己的代码生成器产物) | loop_gen/.../lomanage/local_promptmanageservice.go(Day 05 的 LocalPromptManageService) |
一句话记住区别:
kitex_gen 是"标准答案"(Thrift 官方生成器产出的类型和接口),loop_gen 是"Coze Loop 自己加的辅助工具"(在标准答案基础上,为"模块间用本地调用而不是真 RPC"这个特殊需求量身定制的一层)。两者都不能手改,但 loop_gen 是本仓库特有的设计,其它用 Kitex 的项目未必有。L05
thrift → Go → TS:三步生成流程
docs/guidance/idl-codegen-guide.md 定义的标准流程,改一次接口需要走完这条链:
① 改 .thrift唯一手写的一步→
② kitex_tool.sh生成 kitex_gen→
③ hertz_tool.sh生成路由→
④ code_gen.sh生成 loop_gen→
⑤ frontend/infra/idl生成 TS 类型
cd backend
bash script/cloudwego/kitex_tool.sh # 生成 backend/kitex_gen/
bash script/cloudwego/hertz_tool.sh # 生成 backend/api/router_gen.go
bash script/cloudwego/code_gen.sh # 生成 backend/loop_gen/ 等通用代码
# 前端通过 infra/idl 工具把 thrift 转成 TS 类型,产物进 packages/loop-base/api-schema/
⚠️ 坑:新增服务接口后,还要记得重新跑 Day 06 的 wire
如果这次 IDL 变更新增了一个 Service(而不只是加字段),对应模块的
application/ 目录下可能需要重新执行 wire 生成 wire_gen.go——IDL 生成和 Wire 生成是两条独立但常常连在一起需要跑的流水线,容易漏掉后者导致编译失败。数据库 schema 变更还要额外跑 GORM 模型生成、错误码生成——idl-codegen-guide.md 第 65-79 行有完整清单。L06
今日小结 + 动手
🧠 今天你应该能回答
- 为什么要契约先行?(避免前后端字段/类型不一致,一份定义生成两端类型)
- idl/thrift 目录怎么组织?(按六大模块分子目录,domain/ 放纯数据结构)
- kitex_gen 和 loop_gen 的区别?(前者是 Thrift 标准生成物,后者是本地调用客户端的额外生成层)
- 改一次接口要跑哪些生成脚本?(kitex_tool.sh → hertz_tool.sh → code_gen.sh → 前端 idl 转换 → 可能还要重新 wire)
✋ 动手 5 分钟
# 1. 看 prompt 模块的 IDL 文件清单
ls idl/thrift/coze/loop/prompt/
# 2. 找 CreatePrompt 的原始契约定义
grep -n "CreatePrompt" idl/thrift/coze/loop/prompt/coze.loop.prompt.manage.thrift
# 3. 对比生成出来的 Go 类型
grep -n "type CreatePromptRequest struct" -A 5 backend/kitex_gen/coze/loop/prompt/manage/*.go
# 4. 看生成脚本长什么样(不需要真的执行)
cat backend/script/cloudwego/kitex_tool.sh
明天预告 · Day 08:今天看到几乎每个 Service 请求都带
workspace_id,这背后是 foundation 模块管的"用户/空间/鉴权"体系。明天专门拆开它,看为什么所有业务模块都要先过这一层。