docs: refine AI scene routing spec

This commit is contained in:
2026-05-22 22:33:14 +08:00
parent 8a4486801f
commit dfbf68989b
@@ -75,8 +75,37 @@ aiRuntimeService.invoke("diary_summary", params, userId);
-> 写 ai_call_log
```
### 模块边界
后端新增以下核心单元,避免继续把服务商差异堆在 `AiChatServiceImpl`
| 单元 | 职责 | 不负责 |
| --- | --- | --- |
| `AiRuntimeService` | 按 `sceneCode` 查询绑定、选择接口配置、执行 fallback、返回统一结果 | 不关心 Dify/Coze 请求细节 |
| `AiProviderAdapter` | 服务商适配器接口,定义组包、调用、解析的统一契约 | 不读取业务场景 |
| `DifyProviderAdapter` | 处理 Dify `/chat-messages` 的 blocking/streaming 请求和响应解析 | 不处理 Coze 格式 |
| `CozeProviderAdapter` | 迁移现有 Coze workflow 请求、SSE 解析和错误处理 | 不处理 Dify 格式 |
| `AiTemplateRenderer` | 渲染请求模板,合并场景输入、endpoint 默认值和运行时变量 | 不发送 HTTP |
| `AiCallLogService` | 统一写入调用日志和测试日志,敏感字段脱敏 | 不决定业务结果 |
业务服务只依赖 `AiRuntimeService`,不直接依赖 `DifyProviderAdapter``CozeProviderAdapter` 或数据库配置表。
## 数据模型
所有新增表都遵循项目已有公共字段约定:
```text
id
create_by
create_time
update_by
update_time
is_deleted
remarks
```
删除操作使用逻辑删除。查询默认过滤 `is_deleted = 0`
### ai_provider
服务商账号和基础能力配置。
@@ -102,6 +131,16 @@ aiRuntimeService.invoke("diary_summary", params, userId);
| `is_enabled` | 是否启用 |
| `description` | 描述 |
约束和索引:
```text
UNIQUE(provider_code)
INDEX(provider_type, is_enabled)
INDEX(environment, is_enabled)
```
同一 `provider_code` 逻辑删除后不可直接复用,避免历史调用日志无法追溯。需要复用时通过恢复或显式改名处理。
### ai_endpoint_config
具体接口、工作流或模型调用配置。
@@ -127,9 +166,31 @@ aiRuntimeService.invoke("diary_summary", params, userId);
| `timeout_ms` | 接口级超时时间 |
| `retry_count` | 接口级重试次数 |
| `retry_delay_ms` | 接口级重试延迟 |
| `last_test_status` | 最近一次测试状态 |
| `last_test_time` | 最近一次测试时间 |
| `last_test_trace_id` | 最近一次测试链路 ID |
| `is_enabled` | 是否启用 |
| `description` | 描述 |
约束和索引:
```text
UNIQUE(endpoint_code)
INDEX(provider_id, is_enabled)
INDEX(endpoint_type, is_enabled)
```
`path``full_url_override` 的优先级:
1. 如果 `full_url_override` 非空,直接使用该 URL。
2. 否则使用 `ai_provider.base_url + ai_endpoint_config.path`
`timeout_ms``retry_count``retry_delay_ms` 的优先级:
```text
endpoint 配置值 > provider 默认值 > 系统默认值
```
### ai_scene_binding
业务场景到接口配置的绑定。
@@ -147,8 +208,24 @@ aiRuntimeService.invoke("diary_summary", params, userId);
| `priority` | 预留优先级 |
| `input_schema` | JSON,说明入参结构 |
| `test_payload` | JSON,后台场景测试默认入参 |
| `last_test_status` | 最近一次场景测试状态 |
| `last_test_time` | 最近一次场景测试时间 |
| `last_test_trace_id` | 最近一次场景测试链路 ID |
| `description` | 描述 |
约束和索引:
```text
UNIQUE(scene_code, environment)
INDEX(endpoint_config_id)
INDEX(fallback_endpoint_id)
INDEX(scene_category, is_enabled)
```
同一环境内一个 `scene_code` 只能有一条生效绑定。后续如果需要灰度或权重路由,另行增加路由规则表,不在本期混入 `ai_scene_binding`
本期不单独新增场景字典表,`ai_scene_binding` 同时承担“场景清单”和“环境绑定”职责。初始化时为每个支持的 `scene_code` 按环境创建一条记录,`endpoint_config_id` 可先为空,`is_enabled = 0`。后台场景页必须展示这些未绑定场景,管理员完成 endpoint 绑定、测试通过并启用后才参与运行时路由。
### ai_call_log
统一调用日志,替代后续新增场景对 `t_coze_api_call` 的依赖。
@@ -173,6 +250,18 @@ aiRuntimeService.invoke("diary_summary", params, userId);
| `error_code` | 错误码 |
| `error_message` | 错误信息 |
索引:
```text
INDEX(trace_id)
INDEX(scene_code, create_time)
INDEX(provider_type, create_time)
INDEX(endpoint_config_id, create_time)
INDEX(status, create_time)
```
日志中 `request_headers` 必须脱敏 `Authorization``api-key``x-api-key``client_secret` 等敏感字段。
## 初始场景编码
| sceneCode | 场景 |
@@ -187,6 +276,14 @@ aiRuntimeService.invoke("diary_summary", params, userId);
后续新增场景只需要新增 `ai_scene_binding` 和业务调用点,不需要新增服务商专用字段。
新增场景的规则:
1. 先在代码中定义稳定 `sceneCode` 常量,业务调用只引用该常量。
2. 数据初始化脚本为所有环境创建对应 `ai_scene_binding` 记录,默认禁用且不绑定 endpoint。
3. 后台展示未绑定场景,并允许管理员选择 endpoint、填写 `test_payload`、执行场景测试。
4. 只有绑定的 endpoint、provider 都启用,且场景本身启用后,运行时才会调用。
5. 如果业务调用了未初始化的 `sceneCode`,返回 `AI_SCENE_NOT_FOUND` 并写入失败日志。
## Dify 适配
依据 `docs/dify平台接口.md`,本期优先支持:
@@ -217,7 +314,20 @@ Dify 请求体模板应支持:
}
```
流式响应后续可支持 SSE 事件解析,优先提取 `message``message_end` 中的内容。
流式响应本期定义为可测试、可解析,但业务是否启用由 endpoint 的 `support_stream` 和请求模板共同决定。Dify SSE 解析规则如下:
```json
{
"mode": "sse",
"eventField": "event",
"contentEvents": ["message"],
"contentPath": "$.answer",
"endEvents": ["message_end"],
"errorEvents": ["error"]
}
```
`DifyProviderAdapter` 在 blocking 模式下返回 `$.answer`,在 streaming 模式下拼接所有 `event=message``answer` 内容;遇到 `event=error` 时标记调用失败并写入 `ai_call_log.error_message`
## Coze 适配
@@ -228,6 +338,106 @@ Dify 请求体模板应支持:
- 保留现有重试和异常处理策略。
- 新日志写入 `ai_call_log`,旧 `t_coze_api_call` 可在过渡期继续写入。
Coze 默认请求模板:
```json
{
"bot_id": "{{botId}}",
"workflow_id": "{{workflowId}}",
"user_id": "{{userId}}",
"stream": true,
"additional_messages": [
{
"role": "user",
"content": "{{input}}",
"content_type": "text",
"type": "question"
}
],
"parameters": "{{parameters}}"
}
```
Coze 响应解析先沿用现有 `AiChatServiceImpl` 中的 SSE 解析逻辑,提取 End 节点 `content.output` 或文本回答;迁移完成后该逻辑归属 `CozeProviderAdapter`
## 请求模板与响应解析
### 模板变量
`request_template` 使用简单占位符语法,避免引入复杂脚本执行:
```text
{{input}} 单文本输入
{{userId}} 当前用户 ID
{{conversationId}} 会话 ID,可为空
{{workflowId}} endpoint.workflow_id
{{botId}} endpoint.bot_id
{{parameters}} 运行时参数对象
{{inputs.xxx}} 运行时参数中的 xxx 字段
```
渲染规则:
1. 先加载 endpoint 的 `request_template`
2. 注入 endpoint 字段,例如 `workflowId``botId`
3. 注入运行时参数 `inputs`
4. 注入系统参数,例如 `userId``conversationId``traceId`
5. 渲染完成后必须校验为合法 JSON。
如果某个必填占位符缺失,测试和运行时调用都直接失败,并返回明确错误,例如 `AI_TEMPLATE_VARIABLE_MISSING: workflowId`
### 响应解析
`response_parser` 支持三类模式:
```json
{
"mode": "json_path",
"answerPath": "$.answer"
}
```
```json
{
"mode": "sse",
"contentEvents": ["message"],
"contentPath": "$.answer",
"endEvents": ["message_end"],
"errorEvents": ["error"]
}
```
```json
{
"mode": "raw_text"
}
```
解析失败时:
1. `ai_call_log.status = failed`
2. `error_code = AI_RESPONSE_PARSE_ERROR`
3. 日志保留原始响应。
4. 如果场景配置了 fallback,则继续调用 fallback。
统一错误码:
| 错误码 | 场景 |
| --- | --- |
| `AI_SCENE_NOT_FOUND` | 未找到当前环境的场景配置 |
| `AI_SCENE_DISABLED` | 场景被禁用 |
| `AI_ENDPOINT_NOT_FOUND` | endpoint 不存在或已删除 |
| `AI_ENDPOINT_DISABLED` | endpoint 被禁用 |
| `AI_PROVIDER_NOT_FOUND` | provider 不存在或已删除 |
| `AI_PROVIDER_DISABLED` | provider 被禁用 |
| `AI_TEMPLATE_VARIABLE_MISSING` | 请求模板缺少必填变量 |
| `AI_TEMPLATE_INVALID_JSON` | 请求模板渲染后不是合法 JSON |
| `AI_PROVIDER_REQUEST_FAILED` | 外部服务调用失败 |
| `AI_RESPONSE_PARSE_ERROR` | 响应解析失败 |
| `AI_FALLBACK_FAILED` | 主配置和兜底配置都失败 |
业务层收到错误时,只处理统一错误码和用户可见提示,不直接解析 Dify 或 Coze 的原始错误结构。
## 后端接口
新增管理 API 前缀:
@@ -394,6 +604,33 @@ POST /ai/scenes/test
本期不做长期缓存。后续如增加缓存,必须基于 `update_time` 或版本号自动失效。
后台保存配置时不需要重启服务。运行时读取顺序固定为:
```text
scene_code + environment
-> enabled ai_scene_binding
-> enabled ai_endpoint_config
-> enabled ai_provider
```
任一层不存在或禁用,调用失败并写日志,不静默降级到旧配置。
运行环境来源固定为后端当前环境配置,例如 Spring Profile 或项目已有环境变量。业务代码不从前端请求参数读取 environment,避免用户侧伪造环境导致误路由。
### 配置生效与切换保护
立即生效只针对已启用配置。新增或编辑 provider、endpoint、scene 时,保存动作只落库;是否进入运行时由 `is_enabled` 控制。
生产环境建议采用以下切换流程:
1. 保存 provider 或 endpoint 后先保持禁用。
2. 在后台执行 provider 测试、endpoint 测试或 scene 测试。
3. 测试成功后再启用 endpoint 或 scene binding。
4. 切换场景绑定时,后台记录切换前 endpoint、切换后 endpoint、操作者、时间和测试 traceId。
5. 如果切换后出现失败,可直接把 scene 绑定切回上一条 endpoint 配置,下一次调用立即生效。
后台可以先实现为确认弹窗和审计日志,不强制做审批流。若后续生产切换风险升高,再增加“双人复核”或“待发布配置”状态。
### 兜底策略
```text
@@ -411,6 +648,15 @@ POST /ai/scenes/test
- 场景禁用:业务调用直接返回明确错误。
- 后台绑定时应提示禁用配置不可绑定,或允许保存但不可启用。
### 安全与审计
- 管理后台编辑页允许输入完整 Key,但列表页和详情页默认脱敏展示。
- 只有显式“显示密钥”操作才展示完整 Key,并记录管理员操作日志。
- `ai_call_log` 永远不保存明文 Token。
- 测试请求中的请求头展示也必须脱敏。
- 所有配置新增、修改、启用、禁用、场景切换都写入后台操作审计日志。
- 不允许前端直接调用外部 Dify/Coze URL 进行正式测试;正式测试走后端 `/ai/runtime/test`,避免 Token 暴露到浏览器。
## 迁移策略
第一期保留 `t_ai_config`
@@ -442,6 +688,19 @@ Dify 初始化:
迁移后,业务代码逐步从 `callWorkflowByConfigKey(configKey, ...)` 迁移到 `invoke(sceneCode, ...)`
迁移后的兼容规则:
1. 新场景优先走 `AiRuntimeService`
2. 尚未迁移的旧调用继续走 `AiChatServiceImpl``t_ai_config`
3. 如果新路由调用失败,不自动回退到旧 `t_ai_config`,除非该场景显式配置了 fallback endpoint。
4. 每迁移一个业务场景,都要删除该业务中的硬编码 `coze.*` 配置键引用。
回滚方式:
1. 保留旧 `t_ai_config` 和旧调用代码直到所有关键场景验收通过。
2. 如果某个场景新路由异常,可在后台将该 `sceneCode` 绑定切回 Coze endpoint。
3. 如果新 runtime 服务整体异常,可通过配置开关让指定业务临时回退旧方法;该开关只作为过渡保护,不作为长期设计。
## 实施顺序
1. 新增数据库表、实体、Mapper、DTO、Service、Controller。
@@ -454,13 +713,17 @@ Dify 初始化:
8. 将对话、日记总结、情绪分析、疗愈等旧 Coze 场景逐步迁到 `sceneCode`
9. 保留旧 `AiConfig` 页面或提供只读迁移视图,确认稳定后再下线。
每一步交付都应保持系统可启动、已有 Coze 对话不回归。
## 测试计划
后端测试:
- `AiRuntimeServiceTest`:验证场景路由、禁用状态、fallback。
- `DifyProviderAdapterTest`:验证 Dify 请求体生成和 `answer` 解析。
- `DifyProviderAdapterStreamTest`:验证 Dify SSE 的 `message` 拼接、`message_end` 结束和 `error` 失败处理。
- `CozeProviderAdapterTest`:验证 Coze 请求体生成和 SSE 解析。
- `AiTemplateRendererTest`:验证占位符替换、对象注入、缺失变量报错和 JSON 校验。
- 迁移脚本测试:验证旧 `t_ai_config` 能拆成 provider、endpoint、scene。
前端测试:
@@ -469,6 +732,7 @@ Dify 初始化:
- 接口配置可按服务商生成默认请求模板。
- 场景绑定切换后,场景测试走新的接口配置。
- 禁用配置后不能被正常调用。
- 测试请求不在浏览器暴露明文 Token。
集成验收:
@@ -487,6 +751,8 @@ Dify 初始化:
| 立即生效可能导致误操作影响线上 | 增加测试按钮、启用状态、环境隔离和调用日志 |
| 旧业务调用链较长 | 分阶段迁移,旧 `t_ai_config` 保留兼容 |
| Token 暴露风险 | 管理后台可编辑但日志和普通详情默认脱敏 |
| 请求模板配置错误导致线上失败 | 保存时校验 JSON,测试通过后再允许启用场景绑定 |
| 场景切换误操作影响生产 | 环境隔离、操作审计、场景测试和明确启用开关 |
## 验收标准
@@ -497,3 +763,6 @@ Dify 初始化:
5. 业务调用只依赖 `sceneCode`,不再需要知道当前场景使用 Dify 还是 Coze。
6. 修改场景绑定后,下一次业务调用立即走新配置。
7. 调用日志能区分服务商、接口配置、场景、成功/失败和 fallback。
8. Dify blocking 和 streaming 两种响应都能被后台测试解析。
9. 管理后台和调用日志不泄露明文 Token。
10. 旧 Coze 对话在迁移期间保持可用。