跳到主要内容

coze-loop 提示词管理实现

约 15 分钟3,469 字更新于 2026/10/04

返回 概览 · 对照阅读 Langfuse · 横向对比与建议 · 交互页面

  • 仓库: wzqwtt/coze-loop
  • 提交: 3a6a2bf07b057fec0c702e514e8345fb5684e83a(2026-09-28)
  • 范围: 后端 backend/modules/prompt、backend/modules/llm、Thrift IDL、MySQL 初始化 SQL、Hertz 路由与中间件、前端提示词 hooks。
  • 规则: 每条结论都带源码永久链接。行号对应上述提交。

目录​

  1. 结论速览
  2. 术语
  3. 架构
  4. 数据模型
  5. 流程一:创建、草稿、提交、标签
  6. 流程二:SDK / OpenAPI 获取
  7. 流程三:渲染并发送给 LLM
  8. 模板
  9. 缓存与一致性
  10. 风险与缺口

1. 结论速览​

主题结论证据
模型一个提示词有 3 部分:元数据 PromptBasic、每用户草稿 PromptDraft、不可变提交 PromptCommit。prompt.go#L12-L45
生命周期先草稿,后提交。没有单独的"发布"步骤。manage.go#L760-L953
版本版本号必须是严格 semver。application/manage.go#L578-L639
标签标签是发布指针。每个提示词的每个标签只指向 1 个版本。prompt_commit_label_mapping.sql#L1-L19
模板3 种引擎:normal、jinja2、go_template。每次渲染 10 s 超时、1 MB 输出上限。prompt_detail.go#L400-L533
组合snippet 类型的提示词可以被引用。读取时展开,最多 2 层。service/manage.go#L471-L597
获取POST /v1/loop/prompts/mget,PAT 鉴权。版本优先级:显式版本 > 标签 > 最新版本。openapi.go#L383-L577
缓存开源版只有"标签→版本"缓存是真 Redis(TTL 60 s)。提示词缓存是空实现。redis/prompt.go#L26-L38
执行服务端渲染并调用 LLM。进程内调用 llm 模块,底层是 eino。service/execute.go#L61-L227
追踪每一层都发 span,经 cozeloop-go SDK 上报。consts/trace.go#L6-L38

2. 术语​

本文统一使用以下术语。

术语含义代码中的名字
提示词一个可管理的 prompt 对象Prompt
草稿某个用户私有的、可编辑的内容PromptDraft、表 prompt_user_draft
提交不可变的版本快照PromptCommit、表 prompt_commit
版本提交的 semver 版本号version
标签指向某个版本的可移动指针label_key、表 prompt_commit_label_mapping
片段可被其他提示词引用的提示词PromptType = snippet
渲染把变量值填入模板formatMessages / formatText
空间租户隔离单位space_id / workspace_id

3. 架构​

层位置关键类型
IDL 契约idl/thrift/coze/loop/prompt/*.thrift5 个服务:Manage、Debug、Execute、OpenAPI、ToolManage(coze.loop.prompt.thrift#L9-L13)
HTTP 网关backend/api/handler/coze/loop/apis/prompt_*.go一元调用用 invokeAndRender。流式调用转成 SSE(prompt_debug_service.go#L24-L58)
本地 RPC 绑定handler.go#L171-L192Kitex 形状的客户端,实际是进程内适配器
应用层backend/modules/prompt/application/鉴权、审计、DTO↔DO 转换、编排
领域服务domain/service/IPromptService(service.go#L17-L44)
仓储接口domain/repo/IManageRepo(repo/manage.go#L12-L27)
端口(RPC)domain/component/rpc/ILLMProvider(rpc/llm.go#L32-L36)
基础设施infra/repo、infra/rpc、infra/confMySQL(gorm-gen)、Redis、适配器

4. 数据模型​

4.1 领域实体​

4.2 MySQL 表​

  • Docker 初始化 SQL 在 release/deployment/docker-compose/bootstrap/mysql-init/init-sql/。Helm 有一份镜像。
  • PromptDetail 不做范式化。每个子对象序列化成独立的 JSON 文本列。convertor/manage.go#L158-L200
表用途关键约束
prompt_basic每个提示词 1 行头记录UNIQUE(space_id,prompt_key,deleted_at),软删除
prompt_user_draft每用户每提示词 1 份草稿UNIQUE(prompt_id,user_id,deleted_at)
prompt_commit不可变版本快照UNIQUE(prompt_id,version),无软删除
prompt_label空间级标签定义UNIQUE(space_id,label_key,deleted_at)
prompt_commit_label_mapping标签→版本指针UNIQUE(prompt_id,label_key,deleted_at)
prompt_relation片段引用图双向索引;main_prompt_version='' 表示草稿
prompt_debug_log调试历史token 数、耗时、状态码
prompt_debug_context每用户 Playground 状态UNIQUE(prompt_id,user_id)
tool_commit工具版本UNIQUE(tool_id,version)

4.3 ID​

  • 表声明了 AUTO_INCREMENT。但每次插入都自带 ID。manage.go#L81-L91
  • ID 类似 snowflake:32 位秒 + 10 位毫秒 + 8 位计数器 + 14 位服务器 ID。计数器用 Redis INCRBY。redis_gen.go#L26-L136
  • prompt_key 在空间内唯一。重复时返回 PromptKeyExistCode。prompt_basic.go#L74-L91

5. 流程一:创建、草稿、提交、标签​

5.1 HTTP 接口​

  • 路由定义:manage.thrift#L8-L44。
    • POST /api/prompt/v1/prompts:创建
    • POST .../drafts/save:保存草稿
    • POST .../drafts/commit:提交
    • POST .../drafts/revert_from_commit:从提交回滚草稿
    • POST .../commits/:commit_version/labels_update:移动标签
  • 所有 /api/* 路由经过 SessionMW。middleware.go#L26-L34
  • 前端 usePrompt 每 800 ms 防抖自动保存草稿。use-prompt.ts#L238-L285

5.2 时序​

5.3 步骤细节​

创建(application/manage.go#L127-L189)

  • 默认类型是 normal。默认安全级别是 L3。
  • 审计在领域服务之前运行。
  • 仓储在 1 个事务里写 basic、草稿、草稿引用关系。infra/repo/manage.go#L76-L155

保存草稿(manage.go#L531-L641)

  • 事务先对 prompt_basic 行执行 SELECT … FOR UPDATE。这把同一提示词的并发写串行化。
  • 系统校验 base_version 存在。
  • 新草稿与旧草稿深度相等时,系统不做任何事。
  • 否则系统设置 IsModified = !DeepEqual(草稿, 基线提交)。
  • 系统按 diff 调整片段引用。manage.go#L643-L758
  • 保存草稿不调用审计。

提交(manage.go#L760-L953)

  1. 锁住 basic 行。
  2. 用草稿内容构建提交。BaseVersion 取草稿的基线版本。
  3. 插入 prompt_commit。版本重复时唯一键报错。
  4. 软删除草稿。
  5. 更新 latest_version。
  6. 把草稿引用关系复制为提交引用关系。
  7. 写入标签映射。
  • 系统只校验版本格式。系统不校验版本递增。

其他操作

5.4 标签​

6. 流程二:SDK / OpenAPI 获取​

6.1 鉴权​

6.2 时序​

  • 接口:POST /v1/loop/prompts/mget。请求体 {workspace_id, queries:[{prompt_key, version?, label?}]}。openapi.thrift#L7-L17

6.3 步骤细节​

  • 限流:每空间默认 500 QPS。配置或限流器出错时放行(fail open)。openapi.go#L579-L599、prompt.yaml#L1-L4
  • 版本解析:显式版本 > 标签 > 最新版本。未提交返回 PromptUncommittedCode。标签未关联返回 PromptLabelUnAssociatedCode。service/manage.go#L221-L307
  • 个人草稿:版本 $Draft 选择调用者自己的草稿。无用户时回退到常量用户 "openAPI"。openapi.go#L473-L485
  • 批量读:把 (prompt_id, version) 对拼成 OR 查询。MGet 调用了 .Debug(),会打印每条 SQL。mysql/prompt_commit.go#L105-L134
  • 管理类 OpenAPI:也提供 Create、Get、SaveDraft、Commit、List。OpenAPI 的提交不接受标签,也不审计。openapi.go#L350-L381
  • 内部获取:评测等内部调用方使用 BatchGetPrompt,按设计不鉴权("内部接口不鉴权")。application/manage.go#L382-L407

7. 流程三:渲染并发送给 LLM​

coze-loop 在服务端渲染并调用模型。入口有 3 个:调试 / Playground、PTaaS 执行、内部执行。

7.1 执行循环​

  • 循环:Execute 和 ExecuteStreaming 共用结构。停止条件:单步模式、50 步、30 分钟、无工具调用。service/execute.go#L61-L227
  • 工具结果只来自 mock。系统从不执行真实工具。tool_results_collector.go#L38-L75
  • 工具选择:tool_choice != none 时才发送工具。specific 只在单步模式下允许。tool_config.go#L27-L51
  • 请求转换:LLMCallParamConvert 生成 runtime.ChatRequest。rpc/convertor/chat.go#L23-L70
    • 工具选择映射被注释掉了("llm暂时不支持toolCallConfig")。
    • Thinking 和 Extra 没有映射。
  • 模型运行时:校验请求 → 查模型 → QPM/TPM 限流(TPM 按 max_tokens 计,不按实际用量)→ 图片转 base64 → 模型 span → 调用 → 异步记录请求。llm/application/runtime.go#L55-L212
  • 协议:只支持 eino 框架。协议有 ark、openai、claude、deepseek、ollama、gemini、qwen、qianfan、arkbot。eino/init.go#L35-L69

7.2 调试 / Playground​

  • 客户端在请求中发送完整提示词。服务端不从数据库重新加载。debug.go#L225-L233
  • id==0 时是 Playground,key 为 playground-<uid>。否则是调试,校验 debug 权限。debug.go#L76-L90
  • 调试日志在 defer 中用 context.WithoutCancel 写入。客户端断开也会保存。debug.go#L255-L267
  • 前端 fetchStream 调用 /api/prompt/v1/prompts/:prompt_id/debug_streaming。use-llm-stream-run.ts#L165-L240

7.3 PTaaS 执行(提示词即服务)​

  • 接口:POST /v1/loop/prompts/execute 和 /execute_streaming。openapi.go#L601-L917
  • 限流:每 (空间, prompt_key) 默认 100 QPS。
  • 版本解析与获取流程相同。
  • 覆盖参数前先深拷贝提示词,避免污染缓存。openapi.go#L1248-L1323
  • 以 SingleStep=true、ScenarioPTaaS 调用 Execute。

7.4 追踪​

Span产生者证据
PromptExecutorDebug、OpenAPI、Execute 应用层debug.go#L91-L136
PromptHub提示词解析openapi.go#L943-L966
Sequence(baggage 带 key 和 version)每一步service/execute.go#L359-L399
PromptTemplate渲染器formatter.go#L37-L60
工具 span每次工具调用service/execute.go#L330-L357
模型 spanllm 运行时runtime.go#L81-L83

8. 模板​

源码:prompt_detail.go#L257-L398。

  1. 组装消息:模板消息在前,运行时消息追加在后。
  2. 按角色渲染:
    • placeholder 被变量中的消息列表替换。
    • tool 不渲染。
    • system、user 默认渲染。
    • assistant 只在 SkipRender 显式为 false 时渲染。
  3. 按引擎渲染文本(prompt_detail.go#L400-L533):
引擎实现行为
normalfasttemplate,{{var}}未定义的标签原样保留。已定义但无值的标签变成空串
jinja2gonja/v2,沙箱变量先按类型转换(整数、浮点、布尔、JSON)
go_templatetext/template同上
custom_template_m无返回 UnsupportedTemplateTypeCode
  1. 沙箱(jinja_template.go#L21-L104、safe_writer.go#L12-L41):
    • 禁用 include、extends、import、from。
    • range 最多 10,000 个元素。
    • 10 s 超时,输出上限 1 MB。
    • 超时后渲染 goroutine 不会被取消。
  2. 片段展开在渲染之前进行。每个引用替换为片段第一条消息的内容。service/manage.go#L514-L533

9. 缓存与一致性​

缓存开源版状态证据
提示词(按 prompt_id + version)空实现。周边逻辑完整:只缓存提交、命中指标、未命中回写redis/prompt.go#L26-L38、manage.go#L235-L300
提示词元数据(按 space_id + prompt_key)空实现。更新、删除、提交时仍调用失效redis/prompt_basic.go#L19-L36
标签→版本真 Redis。键 prompt_label_version:prompt_id=%d:label_key=%s,默认 TTL 60 sredis/prompt_label_version.go#L54-L161、prompt.yaml#L24-L27
读己之写Redis 写标记。写后读走 MySQL 主库prompt_basic.go#L109-L116

10. 风险与缺口​

10.1 并发与一致性​

#问题影响证据
1带标签提交后,系统只失效空实现的 basic 缓存,不失效标签缓存SDK 按标签读取,最多 60 s 内拿到旧版本manage.go#L881-L951
2提交 Get 计算了写标记但丢弃结果写后读可能落到从库mysql/prompt_commit.go#L82-L103
3不校验 semver 递增低版本可以成为 latest_versionmanage.go#L816-L826
4历史分页游标是秒级时间戳,比较含等号同一秒的提交可能跨页重复prompt_commit.go#L140-L160
5草稿每用户私有,无合并或冲突检测最后一次提交决定 latest_versionmanage.go#L531-L641
6流式发送失败时消费者提前返回生产者阻塞在无缓冲通道上,直到上游 context 取消openapi.go#L827-L858

10.2 权限(开源 foundation)​

#问题影响证据
7鉴权只查空间成员关系,忽略动作开源版没有角色模型infra/rpc/auth.go#L30-L53、foundation/application/auth.go#L45-L64
8UpdateCommitLabels 用请求中的 workspace_id 鉴权,不先加载提示词空间 A 的成员似乎可以移动空间 B 提示词的标签application/manage.go#L963-L993
9片段按原始 ID 读取,不校验空间其他空间的片段可能被展开进模型输入service/manage.go#L368-L448
10内部接口 BatchGetPrompt、ExecuteInternal 不鉴权依赖"只在进程内可达"execute.go#L63-L67
11审计和限流出错时放行;审计只查 Content,不查多模态片段内容审核可被绕过infra/rpc/audit.go#L32-L75
12模型 API Key 写入 model_request_record.model_ak每次调用都落库一份密钥runtime.go#L263-L288

10.3 功能缺口​

  • ToolChoice、Thinking、Extra、ResponseAPIConfig 没有传给模型运行时。chat.go#L23-L70
  • 以下只是类型、常量或空实现。商业版似乎在这些扩展点上补齐:
  • 片段展开只用第一条消息内容,最多 2 层,用字符串 ReplaceAll 替换。service/manage.go#L471-L597