ai-robot-core/spec/ai-service/design.md

2141 lines
69 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
feature_id: "AISVC"
title: "Python AI 中台ai-service技术设计"
status: "draft"
version: "0.7.0"
last_updated: "2026-02-27"
inputs:
- "spec/ai-service/requirements.md"
- "spec/ai-service/openapi.provider.yaml"
- "spec/ai-service/openapi.admin.yaml"
- "java/openapi.deps.yaml"
---
# Python AI 中台ai-service技术设计AISVC
## 1. 设计目标与约束
### 1.1 设计目标
- 落地 `POST /ai/chat`**non-streaming JSON****SSE streaming** 两种返回模式,并确保与契约一致:
- non-streaming 响应字段必须包含 `reply/confidence/shouldTransfer`
- streaming 通过 `Accept: text/event-stream` 输出 `message/final/error` 事件序列。
- 实现 AI 侧会话记忆:基于 `(tenantId, sessionId)` 持久化与加载。
- 实现 RAGMVP向量检索并预留图谱检索Neo4j插件点。
- 多租户隔离:
- Qdrant一租户一 collection或一租户一 collection 前缀)。
- PostgreSQL`tenant_id` 分区/索引,保证跨租户不可见。
### 1.2 硬约束(来自契约与需求)
- API 对齐:`/ai/chat`、`/ai/health` 的路径/方法/状态码与 Java 侧 deps 对齐。
- 多租户:请求必须携带 `X-Tenant-Id`(网关/拦截器易处理),所有数据访问必须以 `tenant_id` 过滤。
- SSE事件类型固定为 `message/final/error`,并保证顺序与异常语义清晰。
---
## 2. 总体架构与模块分层
### 2.1 分层概览
本服务按“职责单一 + 可插拔”的原则分为五层:
1) **API 层Transport / Controller**
- 职责:
- HTTP 请求解析、参数校验(含 `X-Tenant-Id`)、鉴权/限流(如后续需要)。
- 根据 `Accept` 头选择 non-streaming 或 SSE streaming。
- 统一错误映射为 `ErrorResponse`
- 输入:`X-Tenant-Id` header + `ChatRequest` body。
- 输出:
- JSON`ChatResponse`
- SSE`message/final/error` 事件流。
2) **编排层Orchestrator / Use Case**
- 职责:
- 整体流程编排:加载会话记忆 → 合并上下文 →可选RAG 检索 → 组装 prompt → 调用 LLM → 计算置信度与转人工建议 → 写回记忆。
- 在 streaming 模式下,将 LLM 的增量输出转为 SSE `message` 事件,同时维护最终 `reply`
- 输入:`tenantId, sessionId, currentMessage, channelType, history?, metadata?`
- 输出:
- non-streaming一次性 `ChatResponse`
- streaming增量 token或片段流 + 最终 `ChatResponse`
3) **记忆层Memory**
- 职责:
- 持久化会话消息与摘要/记忆(最小:消息列表)。
- 提供按 `(tenantId, sessionId)` 查询的会话上下文读取 API。
- 存储PostgreSQL。
4) **检索层Retrieval**
- 职责:
- 提供统一 `Retriever` 抽象接口。
- MVP 实现向量检索Qdrant
- 插件点图谱检索Neo4j实现可新增而不改动 Orchestrator。
5) **LLM 适配层LLM Adapter**
- 职责:
- 屏蔽不同 LLM 提供方差异(请求格式、流式回调、重试策略)。
- 提供:一次性生成接口 + 流式生成接口yield token/delta
### 2.2 关键数据流(文字版)
- API 层接收请求 → 提取 `tenantId`Header与 body → 调用 Orchestrator。
- Orchestrator
1) Memory.load(tenantId, sessionId)
2) merge_context(local_history, external_history)
3) Retrieval.retrieve(query, tenantId, channelType, metadata)MVP 向量检索)
4) build_prompt(merged_history, retrieved_docs, currentMessage)
5) LLM.generate(...)non-streaming或 LLM.stream_generate(...)streaming
6) compute_confidence(…)
7) Memory.append(tenantId, sessionId, user/assistant messages)
8) 返回 `ChatResponse`(或通过 SSE 输出)。
---
## 3. API 与协议设计要点
### 3.1 tenantId 放置与处理
- **主入口**`X-Tenant-Id` header契约已声明 required
- Orchestrator 与所有下游组件调用均显式传入 `tenantId`
- 禁止使用仅 `sessionId` 定位会话,必须 `(tenantId, sessionId)`
### 3.2 streaming / non-streaming 模式判定
-`Accept` 头作为唯一判定依据:
- `Accept: text/event-stream` → SSE streaming。
- 其他 → non-streaming JSON。
---
## 4. RAG 管道设计
### 4.1 MVP向量检索Qdrant流程
#### 4.1.1 步骤
1) **Query 规范化**
- 输入:`currentMessage`(可结合 `channelType` 与 metadata
- 规则:去噪、截断(防止超长)、可选的 query rewriteMVP 可不做)。
2) **Embedding**
-`EmbeddingProvider` 生成向量(可复用 LLM 适配层或独立适配层)。
- ✅ 已确认Token 计数统一使用 `tiktoken` 进行精确计算(用于 history 截断与证据预算)。
3) **向量检索**Qdrant
- 按租户隔离选择 collection见 5.1)。
- 使用 topK + score threshold 过滤。
4) **上下文构建**
- 将检索结果转为 “证据片段列表”,限制总 token 与片段数。
- 生成 prompt 时区分:系统指令 / 对话历史 / 证据 / 当前问题。
5) **生成与引用策略**
- 生成回答必须优先依据证据。
- 若证据不足:触发兜底策略(见 4.3)。
#### 4.1.2 关键参数MVP 默认,可配置)
- topK例如 5~10
- scoreThreshold相似度阈值
- minHits最小命中文档数
- maxEvidenceTokens证据总 token 上限)
### 4.2 图谱检索插件点Neo4j
#### 4.2.1 Retriever 抽象接口(概念设计)
设计统一接口,使 Orchestrator 不关心向量/图谱差异:
- `Retriever.retrieve(ctx) -> RetrievalResult`
- 输入 `ctx`:包含 `tenantId`, `query`, `sessionId`, `channelType`, `metadata` 等。
- 输出 `RetrievalResult`
- `hits[]`:证据条目(统一为 text + score + source + metadata
- `diagnostics`:检索调试信息(可选)
MVP 提供 `VectorRetriever(Qdrant)`
#### 4.2.2 Neo4j 接入方式(未来扩展)
新增实现类 `GraphRetriever(Neo4j)`,实现同一接口:
- tenant 隔离Neo4j 可采用 database per tenant / label+tenantId 过滤 / subgraph per tenant视规模与授权能力选择
- 输出同构 `RetrievalResult`,由 ContextBuilder 使用。
> 约束:新增 GraphRetriever 不应要求修改 API 层与 Orchestrator 的业务流程,只需配置切换(策略模式/依赖注入)。
### 4.3 检索不中兜底与置信度策略(对应 AC-AISVC-17/18/19
定义“检索不足”的判定:
- `hits.size < minHits``max(score) < scoreThreshold` 或 evidence token 超限导致可用证据过少。
兜底动作:
1) 回复策略:
- 明确表达“未从知识库确认/建议咨询人工/提供可执行下一步”。
- 避免编造具体事实性结论。
2) 置信度:
-`T_low` 为阈值(可配置),检索不足场景通常产生较低 `confidence`
3) 转人工建议:
- `confidence < T_low``shouldTransfer=true`,可附 `transferReason`
- ✅ 已确认MVP 阶段 `confidence` 优先基于 RAG 检索分数Score计算并结合检索不中兜底下调
---
## 5. 多租户隔离方案
### 5.1 Qdrant向量库隔离一租户一 Collection
#### 5.1.1 命名规则
- collection 命名:`kb_{tenantId}`(或 `kb_{tenantId}_{kbName}` 为未来多知识库预留)。
#### 5.1.2 读写路径
- 所有 upsert/search 操作必须先基于 `tenantId` 解析目标 collection。
- 禁止在同一 collection 内通过 payload filter 做租户隔离作为默认方案(可作为兜底/迁移手段),原因:
- 更容易出现误用导致跨租户泄露。
- 运维与配额更难隔离(单租户删除、重建、统计)。
#### 5.1.3 租户生命周期
- tenant 创建:初始化 collection含向量维度与 index 参数)。
- ✅ 已确认:采用**提前预置**模式,不通过业务请求动态创建 collection。
- tenant 删除:删除 collection。
- tenant 扩容:独立配置 HNSW 参数或分片(依赖 Qdrant 部署模式)。
### 5.2 PostgreSQL会话库分区与约束
#### 5.2.1 表设计(概念)
- `chat_sessions`
- `tenant_id` (NOT NULL)
- `session_id` (NOT NULL)
- `created_at`, `updated_at`
- 主键/唯一约束:`(tenant_id, session_id)`
- `chat_messages`
- `tenant_id` (NOT NULL)
- `session_id` (NOT NULL)
- `message_id` (UUID 或 bigserial)
- `role` (user/assistant)
- `content` (text)
- `created_at`
#### 5.2.2 分区策略
根据租户规模选择:
**方案 AMVP 推荐):逻辑分区 + 复合索引**
- 不做 PG 分区表。
- 建立索引:
- `chat_messages(tenant_id, session_id, created_at)`
- `chat_sessions(tenant_id, session_id)`
- 好处:实现与运维简单。
**方案 B规模化按 tenant_id 做 LIST/HASH 分区**
- `chat_messages``tenant_id` 分区LIST 或 HASH
- 适合租户数量有限且单租户数据量大,或需要更强隔离与清理效率。
#### 5.2.3 防串租约束
- 所有查询必须带 `tenant_id` 条件;在代码层面提供 `TenantScopedRepository` 强制注入。
- 可选:启用 Row Level SecurityRLS并通过 `SET app.tenant_id` 做隔离(实现复杂度较高,后续可选)。
---
## 6. SSE 状态机设计(顺序与异常保证)
### 6.1 状态机
定义连接级状态:
- `INIT`:已建立连接,尚未输出。
- `STREAMING`:持续输出 `message` 事件。
- `FINAL_SENT`:已输出 `final`,准备关闭。
- `ERROR_SENT`:已输出 `error`,准备关闭。
- `CLOSED`:连接关闭。
### 6.2 事件顺序保证
- 在一次请求生命周期内,事件序列必须满足:
- `message*`0 次或多次) → **且仅一次** `final` → close
-`message*`0 次或多次) → **且仅一次** `error` → close
- 禁止 `final` 之后再发送 `message`
- 禁止同时发送 `final``error`
实现策略(概念):
- Orchestrator 维护一个原子状态变量(或单线程事件循环保证),在发送 `final/error` 时 CAS 切换状态。
- 对 LLM 流式回调进行包装:
- 每个 delta 输出前检查状态必须为 `STREAMING`
- 发生异常立即进入 `ERROR_SENT` 并输出 `error`
### 6.3 异常处理
- 参数错误:在进入流式生成前即可判定,直接发送 `error`(或返回 400取决于是否已经选择 SSE建议 SSE 模式同样用 `event:error` 输出 ErrorResponse
- 下游依赖错误LLM/Qdrant/PG
- 若尚未开始输出:可直接返回 503/500 JSONnon-streaming或发送 `event:error`streaming
- 若已输出部分 `message`:必须以 `event:error` 收尾。
- 客户端断开:
- 立即停止 LLM 流(如果适配层支持 cancel并避免继续写入 response。
- ✅ 已确认:必须实现 SSE 心跳Keep-alive以注释行形式定期发送 `: ping`(不改变事件模型),防止网关/中间件断开连接。
- ✅ 已确认Python 内部设置 **20s 硬超时**(包含 LLM 调用与检索/存储等关键步骤的总体超时控制),防止资源泄露与请求堆积。
---
## 7. 上下文合并规则Java history + 本地持久化 history
### 7.1 合并输入
- `H_local`Memory 层基于 `(tenantId, sessionId)` 读取到的历史(按时间排序)。
- `H_ext`Java 请求中可选的 `history`(按传入顺序)。
### 7.2 去重规则(确定性)
为避免重复注入导致 prompt 膨胀,定义 message 指纹:
- `fingerprint = hash(role + "|" + normalized(content))`
- normalizedtrim + 统一空白MVP 简化trim
去重策略:
1) 先以 `H_local` 构建 `seen` 集合。
2) 遍历 `H_ext`:若 fingerprint 未出现,则追加到 merged否则跳过。
> 解释:优先信任本地持久化历史,外部 history 作为补充。
### 7.3 优先级与冲突处理
-`H_ext``H_local` 在末尾存在重复但内容略有差异:
- MVP 采取“以 local 为准”策略(保持服务端一致性)。
- 将差异记录到 diagnostics可选供后续排查。
### 7.4 截断策略(控制 token
合并后历史 `H_merged` 需受 token 预算约束:
- 预算 = `maxHistoryTokens`(可配置)。
- 截断策略:保留最近的 N 条(从尾部向前累加 token 直到阈值)。
- 可选增强(后续):对更早历史做摘要并作为系统记忆注入。
---
## 8. 关键接口(内部)与可插拔点
### 8.1 Orchestrator 依赖接口(概念)
- `MemoryStore`
- `load_history(tenantId, sessionId) -> messages[]`
- `append_messages(tenantId, sessionId, messages[])`
- `Retriever`
- `retrieve(tenantId, query, metadata) -> RetrievalResult`
- `LLMClient`
- `generate(prompt, params) -> text`
- `stream_generate(prompt, params) -> iterator[delta]`
### 8.2 插件点
- RetrievalVectorRetriever / GraphRetriever / HybridRetriever
- LLMOpenAICompatibleClient / LocalModelClient
- ConfidencePolicy可替换策略基于检索质量 + 模型信号)
---
## 9. 风险与后续工作
- SSE 的网关兼容性:需确认网关是否支持 `text/event-stream` 透传与超时策略。
- 租户级 collection 数量增长若租户数量巨大Qdrant collection 管理成本上升;可在规模化阶段切换为“单 collection + payload tenant filter”并加强隔离校验。
- 上下文膨胀:仅截断可能影响长会话体验;后续可引入摘要记忆与检索式记忆。
- 置信度定义MVP 先以规则/阈值实现,后续引入离线评测与校准。
---
## 10. v0.6.0 智能客服增强 — 总体架构升级
### 10.1 升级后的 Orchestrator 数据流
原有 8 步 pipeline 升级为 12 步,新增步骤用 `[NEW]` 标记:
```
API 层接收请求 → 提取 tenantId + body → 调用 Orchestrator
Orchestrator:
1) Memory.load(tenantId, sessionId)
2) merge_context(local_history, external_history)
3) [NEW] InputGuardrail.scan(currentMessage) → 前置禁词检测(仅记录,不阻断)
4) [NEW] FlowEngine.check_active_flow(sessionId) → 检查是否有进行中的话术流程
├─ 有活跃流程 → FlowEngine.advance(user_input) → 返回话术内容 → 跳到步骤 11
└─ 无活跃流程 → 继续步骤 5
5) [NEW] IntentRouter.match(currentMessage, tenantId) → 意图识别(关键词+正则)
├─ fixed → 返回固定回复 → 跳到步骤 11
├─ flow → FlowEngine.start(flowId, sessionId) → 返回首步话术 → 跳到步骤 11
├─ transfer → shouldTransfer=true + 转人工话术 → 跳到步骤 11
├─ rag → 设置 target_kb_ids → 继续步骤 6
└─ 未命中 → target_kb_ids=按优先级全部 → 继续步骤 6
6) [NEW] QueryRewriter.rewrite(currentMessage, history) → Query 改写LLM 调用,解析指代词)
7) Retrieval.retrieve(rewritten_query, tenantId, target_kb_ids) → 多知识库定向检索
8) [NEW] ResultRanker.rank(hits, kb_priorities) → 分层排序(按知识库类型优先级)
9) [NEW] PromptBuilder.build(template, evidence, history, message) → 从数据库模板构建 Prompt
10) LLM.generate(messages) 或 LLM.stream_generate(messages)
11) [NEW] OutputGuardrail.filter(reply) → 后置禁词过滤mask/replace/block
12) compute_confidence(retrieval_result)
13) Memory.append(tenantId, sessionId, user + assistant messages)
14) 返回 ChatResponse
```
### 10.2 新增模块与现有模块的关系
```
app/
├── services/
│ ├── orchestrator.py # [修改] 升级为 12 步 pipeline
│ ├── prompt/ # [新增] Prompt 模板服务
│ │ ├── template_service.py # 模板 CRUD + 版本管理 + 缓存
│ │ └── variable_resolver.py # 变量替换引擎
│ ├── intent/ # [新增] 意图识别与路由
│ │ ├── router.py # IntentRouter规则匹配引擎
│ │ └── rule_service.py # 规则 CRUD
│ ├── flow/ # [新增] 话术流程引擎
│ │ ├── engine.py # FlowEngine状态机执行
│ │ └── flow_service.py # 流程 CRUD
│ ├── guardrail/ # [新增] 输出护栏
│ │ ├── input_scanner.py # 输入前置检测
│ │ ├── output_filter.py # 输出后置过滤
│ │ └── word_service.py # 禁词/行为规则 CRUD
│ ├── retrieval/
│ │ ├── optimized_retriever.py # [修改] 支持 target_kb_ids 参数
│ │ ├── query_rewriter.py # [新增] Query 改写
│ │ └── result_ranker.py # [新增] 分层排序
│ ├── kb.py # [修改] 支持多知识库 CRUD
│ └── ...(现有模块不变)
├── api/
│ └── admin/
│ ├── prompt_templates.py # [新增] Prompt 模板管理 API
│ ├── intent_rules.py # [新增] 意图规则管理 API
│ ├── script_flows.py # [新增] 话术流程管理 API
│ ├── guardrails.py # [新增] 护栏管理 API禁词+行为规则)
│ ├── kb.py # [修改] 新增知识库 CRUD 端点
│ └── ...(现有 API 不变)
├── models/
│ └── entities.py # [修改] 新增实体定义
└── core/
└── prompts.py # [修改] 改为从数据库加载,保留硬编码作为 fallback
```
---
## 11. Prompt 模板系统设计
### 11.1 数据模型
```
prompt_templates 表
├── id: UUID (PK)
├── tenant_id: VARCHAR (NOT NULL, FK)
├── name: VARCHAR (模板名称,如"默认客服人设")
├── scene: VARCHAR (场景标签chat/rag_qa/greeting/farewell)
├── description: TEXT (模板描述)
├── is_default: BOOLEAN (是否为该场景的默认模板)
├── created_at: TIMESTAMP
├── updated_at: TIMESTAMP
└── INDEX: (tenant_id, scene)
prompt_template_versions 表
├── id: UUID (PK)
├── template_id: UUID (FK → prompt_templates.id)
├── version: INTEGER (自增版本号)
├── status: VARCHAR (draft/published/archived)
├── system_instruction: TEXT (系统指令内容,支持 {{variable}} 占位符)
├── variables: JSONB (变量定义列表,如 [{"name":"persona_name","default":"小N","description":"人设名称"}])
├── created_at: TIMESTAMP
└── INDEX: (template_id, status)
└── UNIQUE: 同一 template_id 下仅一个 status=published
```
### 11.2 变量替换引擎
内置变量(系统自动注入,无需用户定义):
| 变量 | 说明 | 示例值 |
|------|------|--------|
| `{{persona_name}}` | 人设名称 | 小N |
| `{{current_time}}` | 当前时间 | 2026-02-27 14:30 |
| `{{channel_type}}` | 渠道类型 | wechat |
| `{{tenant_name}}` | 租户名称 | 某某公司 |
| `{{session_id}}` | 会话ID | kf_001_wx123 |
自定义变量:由模板定义,管理员在模板中声明变量名和默认值。
替换流程:
1. 加载已发布版本的 `system_instruction`
2. 合并内置变量 + 自定义变量默认值
3. 执行 `{{variable}}` 模式替换
4. 注入行为规则(从 guardrails 加载,追加到系统指令末尾)
5. 输出最终 system message
### 11.3 缓存策略
- 使用内存缓存dictkey = `(tenant_id, scene)`value = 已发布版本的完整模板
- 发布/回滚操作时主动失效缓存
- 缓存 TTL = 300s兜底过期防止分布式场景下缓存不一致
- fallback缓存未命中且数据库无模板时使用现有硬编码的 `SYSTEM_PROMPT` 作为兜底
---
## 12. 多知识库设计
### 12.1 数据模型
```
knowledge_bases 表(扩展现有 KnowledgeBase 实体)
├── id: VARCHAR (PK, 如 "kb_product_001")
├── tenant_id: VARCHAR (NOT NULL)
├── name: VARCHAR (知识库名称)
├── kb_type: VARCHAR (product/faq/script/policy/general)
├── description: TEXT
├── priority: INTEGER (优先级权重,数值越大越优先,默认 0)
├── is_enabled: BOOLEAN (默认 true)
├── doc_count: INTEGER (文档数量,冗余统计)
├── created_at: TIMESTAMP
├── updated_at: TIMESTAMP
└── INDEX: (tenant_id, kb_type)
```
### 12.2 Qdrant Collection 命名升级
现有:`kb_{tenant_id}`(单 collection
升级为:`kb_{tenant_id}_{kb_id}`(每个知识库独立 collection
兼容策略:
- 新创建的知识库使用新命名
- 现有 `kb_{tenant_id}` collection 映射为 `kb_default` 知识库(自动迁移)
- 检索时如果 target_kb_ids 包含 `kb_default`,同时搜索新旧两种命名的 collection
### 12.3 多知识库检索流程
```
target_kb_ids来自意图路由或默认全部
→ 按 kb_type 优先级排序script > faq > product > policy > general
→ 并行检索各 collection使用现有 OptimizedRetriever
→ 合并结果,按 (kb_type_priority, score) 双维度排序
→ 截断到 maxEvidenceTokens
→ 输出 ranked_hits
```
---
## 13. 意图识别与规则引擎设计
### 13.1 数据模型
```
intent_rules 表
├── id: UUID (PK)
├── tenant_id: VARCHAR (NOT NULL)
├── name: VARCHAR (意图名称,如"退货意图")
├── keywords: JSONB (关键词列表,如 ["退货","退款","不想要了"])
├── patterns: JSONB (正则模式列表,如 ["退.*货","怎么退"])
├── priority: INTEGER (优先级,数值越大越先匹配)
├── response_type: VARCHAR (flow/rag/fixed/transfer)
├── target_kb_ids: JSONB (rag 类型时关联的知识库 ID 列表)
├── flow_id: UUID (flow 类型时关联的流程 ID)
├── fixed_reply: TEXT (fixed 类型时的固定回复内容)
├── transfer_message: TEXT (transfer 类型时的转人工话术)
├── is_enabled: BOOLEAN (默认 true)
├── hit_count: BIGINT (命中统计,默认 0)
├── created_at: TIMESTAMP
├── updated_at: TIMESTAMP
└── INDEX: (tenant_id, is_enabled, priority DESC)
```
### 13.2 匹配算法
```python
class IntentRouter:
def match(self, message: str, tenant_id: str) -> Optional[IntentMatchResult]:
rules = self._load_rules(tenant_id) # 按 priority DESC 排序,已缓存
for rule in rules:
if not rule.is_enabled:
continue
# 1. 关键词匹配(任一命中即匹配)
for keyword in rule.keywords:
if keyword in message:
return IntentMatchResult(rule=rule, match_type="keyword", matched=keyword)
# 2. 正则匹配(任一命中即匹配)
for pattern in rule.patterns:
if re.search(pattern, message):
return IntentMatchResult(rule=rule, match_type="regex", matched=pattern)
return None # 未命中,走默认 RAG
```
### 13.3 缓存策略
- 规则列表按 `tenant_id` 缓存在内存中
- 规则 CRUD 操作时主动失效缓存
- 缓存 TTL = 60s
---
## 14. 话术流程引擎设计
### 14.1 数据模型
```
script_flows 表
├── id: UUID (PK)
├── tenant_id: VARCHAR (NOT NULL)
├── name: VARCHAR (流程名称)
├── description: TEXT
├── steps: JSONB (步骤列表,见下方结构)
├── is_enabled: BOOLEAN (默认 true)
├── created_at: TIMESTAMP
├── updated_at: TIMESTAMP
└── INDEX: (tenant_id)
steps JSONB 结构:
[
{
"step_no": 1,
"content": "您好,请问您的订单号是多少?",
"wait_input": true,
"timeout_seconds": 120,
"timeout_action": "repeat", // repeat/skip/transfer
"next_conditions": [
{"keywords": ["不知道","忘了"], "goto_step": 3},
{"pattern": "\\d{10,}", "goto_step": 2}
],
"default_next": 2 // 无条件匹配时的下一步
},
...
]
flow_instances 表(运行时状态)
├── id: UUID (PK)
├── tenant_id: VARCHAR (NOT NULL)
├── session_id: VARCHAR (NOT NULL)
├── flow_id: UUID (FK → script_flows.id)
├── current_step: INTEGER (当前步骤序号)
├── status: VARCHAR (active/completed/timeout/cancelled)
├── context: JSONB (流程执行上下文,存储用户输入等)
├── started_at: TIMESTAMP
├── updated_at: TIMESTAMP
├── completed_at: TIMESTAMP (nullable)
└── UNIQUE: (tenant_id, session_id, status='active') -- 同一会话同时只有一个活跃流程
```
### 14.2 状态机
```
┌─────────────┐
│ IDLE │ (无活跃流程)
└──────┬──────┘
│ 意图命中 flow 规则
┌─────────────┐
┌────►│ ACTIVE │◄────┐
│ └──────┬──────┘ │
│ │ │
│ 用户输入匹配条件 │ 用户输入不匹配
│ │ │ → 重复当前步骤
│ ▼ │
│ 推进到下一步 ────────┘
│ │
│ 到达最后一步
│ │
│ ▼
│ ┌─────────────┐
│ │ COMPLETED │
│ └─────────────┘
│ 超时 / 用户触发退出
│ │
│ ▼
│ ┌─────────────┐
└─────│ TIMEOUT / │
│ CANCELLED │
└─────────────┘
```
### 14.3 FlowEngine 核心逻辑
```python
class FlowEngine:
async def check_active_flow(self, tenant_id: str, session_id: str) -> Optional[FlowInstance]:
"""检查会话是否有活跃流程"""
return await self.repo.get_active_instance(tenant_id, session_id)
async def start(self, flow_id: str, tenant_id: str, session_id: str) -> str:
"""启动流程,返回第一步话术"""
flow = await self.repo.get_flow(flow_id)
instance = FlowInstance(flow_id=flow_id, session_id=session_id, current_step=1, status="active")
await self.repo.save_instance(instance)
return flow.steps[0]["content"]
async def advance(self, instance: FlowInstance, user_input: str) -> FlowAdvanceResult:
"""根据用户输入推进流程"""
flow = await self.repo.get_flow(instance.flow_id)
current = flow.steps[instance.current_step - 1]
# 匹配下一步条件
next_step = self._match_next(current, user_input)
if next_step > len(flow.steps):
# 流程结束
instance.status = "completed"
await self.repo.save_instance(instance)
return FlowAdvanceResult(completed=True, message=None)
instance.current_step = next_step
await self.repo.save_instance(instance)
return FlowAdvanceResult(completed=False, message=flow.steps[next_step - 1]["content"])
```
---
## 15. 输出护栏设计
### 15.1 数据模型
```
forbidden_words 表
├── id: UUID (PK)
├── tenant_id: VARCHAR (NOT NULL)
├── word: VARCHAR (禁词)
├── category: VARCHAR (competitor/sensitive/political/custom)
├── strategy: VARCHAR (mask/replace/block)
├── replacement: TEXT (replace 策略时的替换文本)
├── fallback_reply: TEXT (block 策略时的兜底话术)
├── is_enabled: BOOLEAN (默认 true)
├── hit_count: BIGINT (命中统计,默认 0)
├── created_at: TIMESTAMP
├── updated_at: TIMESTAMP
└── INDEX: (tenant_id, is_enabled)
behavior_rules 表
├── id: UUID (PK)
├── tenant_id: VARCHAR (NOT NULL)
├── rule_text: TEXT (行为约束描述,如"不允许承诺具体赔偿金额")
├── category: VARCHAR (compliance/tone/boundary/custom)
├── is_enabled: BOOLEAN (默认 true)
├── created_at: TIMESTAMP
├── updated_at: TIMESTAMP
└── INDEX: (tenant_id, is_enabled)
```
### 15.2 输出过滤流程
```python
class OutputGuardrail:
async def filter(self, reply: str, tenant_id: str) -> GuardrailResult:
words = self._load_words(tenant_id) # 已缓存
triggered = []
filtered_reply = reply
for word in words:
if not word.is_enabled:
continue
if word.word in filtered_reply:
triggered.append(word)
if word.strategy == "block":
# 整条拦截,返回兜底话术
return GuardrailResult(
reply=word.fallback_reply or "抱歉,让我换个方式回答您",
blocked=True,
triggered_words=[w.word for w in triggered]
)
elif word.strategy == "mask":
filtered_reply = filtered_reply.replace(word.word, "*" * len(word.word))
elif word.strategy == "replace":
filtered_reply = filtered_reply.replace(word.word, word.replacement)
return GuardrailResult(
reply=filtered_reply,
blocked=False,
triggered_words=[w.word for w in triggered]
)
```
### 15.3 Streaming 模式下的护栏处理
SSE 流式输出时,禁词过滤需要特殊处理:
- 维护一个滑动窗口缓冲区buffer大小 = 最长禁词长度
- 每次收到 LLM delta 时追加到 buffer
- 当 buffer 长度超过窗口大小时,对已确认安全的前缀执行输出
-`final` 事件前对剩余 buffer 做最终检查
- `block` 策略在流式模式下:检测到禁词后立即停止输出,发送 `error` 事件并附带兜底话术
### 15.4 行为规则注入
行为规则不做运行时检测,而是注入到 Prompt 中作为 LLM 的行为约束:
```
[系统指令]
{模板内容}
[行为约束 - 以下规则必须严格遵守]
1. 不允许承诺具体赔偿金额
2. 不允许透露内部流程
3. 不允许评价竞品
...
```
---
## 16. 智能 RAG 增强设计
### 16.1 Query 改写
```python
class QueryRewriter:
REWRITE_PROMPT = """根据对话历史,改写用户的最新问题,使其语义完整、适合知识库检索。
规则:
- 解析指代词("它"、"这个"等),替换为具体实体
- 补全省略的主语或宾语
- 保持原意,不添加额外信息
- 如果问题已经足够清晰,直接返回原文
对话历史:
{history}
用户最新问题:{query}
改写后的检索查询:"""
async def rewrite(self, query: str, history: list, llm_client: LLMClient) -> str:
if not history or len(history) < 2:
return query # 无历史或历史太短,不改写
messages = [{"role": "user", "content": self.REWRITE_PROMPT.format(
history=self._format_history(history[-6:]), # 最近 3 轮
query=query
)}]
result = await llm_client.generate(messages, max_tokens=100, temperature=0)
return result.content.strip() or query
```
### 16.2 分层排序
```python
KB_TYPE_PRIORITY = {
"script": 50, # 话术模板最高
"faq": 40, # FAQ 次之
"product": 30, # 产品知识
"policy": 20, # 政策规范
"general": 10, # 通用最低
}
class ResultRanker:
def rank(self, hits: list[RetrievalHit], kb_map: dict[str, KnowledgeBase]) -> list[RetrievalHit]:
"""按 (kb_type_priority DESC, score DESC) 双维度排序"""
def sort_key(hit):
kb = kb_map.get(hit.kb_id)
type_priority = KB_TYPE_PRIORITY.get(kb.kb_type, 0) if kb else 0
custom_priority = kb.priority if kb else 0
return (-(type_priority + custom_priority), -hit.score)
return sorted(hits, key=sort_key)
```
---
## 17. 新增数据库实体汇总
v0.6.0 新增以下 SQLModel 实体(均包含 `tenant_id` 字段,遵循现有多租户隔离模式):
| 实体 | 表名 | 用途 |
|------|------|------|
| PromptTemplate | prompt_templates | Prompt 模板主表 |
| PromptTemplateVersion | prompt_template_versions | 模板版本表 |
| KnowledgeBase扩展 | knowledge_bases | 知识库主表(新增 kb_type/priority/is_enabled |
| IntentRule | intent_rules | 意图规则表 |
| ScriptFlow | script_flows | 话术流程定义表 |
| FlowInstance | flow_instances | 流程运行实例表 |
| ForbiddenWord | forbidden_words | 禁词表 |
| BehaviorRule | behavior_rules | 行为规则表 |
---
## 18. v0.6.0 风险与待澄清
- Query 改写的 LLM 调用会增加约 0.5-1s 延迟和额外 token 消耗;可通过配置开关控制是否启用。
- 流式模式下的禁词滑动窗口可能导致输出延迟(等待 buffer 填满);需要在实时性和安全性之间权衡窗口大小。
- 多知识库并行检索会增加 Qdrant 负载;需要评估并发 collection 搜索的性能影响。
- 话术流程的超时检测依赖调用方Java 侧)触发;需要与 Java 侧约定超时回调机制。
- 现有 `kb_default` 到多知识库的数据迁移需要平滑过渡,不能中断现有服务。
---
## 19. v0.7.0 测试与监控系统设计
### 19.1 设计目标与范围
#### 19.1.1 核心目标
- **可测试性**:为 v0.6.0 新增的四大功能Prompt 模板、意图规则、话术流程、输出护栏)提供独立测试能力。
- **可观测性**:提供细粒度的运行时监控数据,支持规则命中率、流程执行状态、护栏拦截统计等。
- **可追溯性**:完整记录对话流程的 12 步执行细节,支持问题排查与效果评估。
- **可导出性**:支持对话数据导出,便于离线分析与模型优化。
#### 19.1.2 设计约束
- **性能优先**:监控数据采集不能显著影响对话生成性能(目标:<5% 延迟增加)。
- **存储可控**完整流程测试的详细日志仅保留 7 避免存储膨胀
- **租户隔离**所有测试与监控数据必须按 `tenant_id` 隔离
- **向后兼容**新增监控不影响现有 `/ai/chat` 接口的行为与性能
---
### 19.2 总体架构
#### 19.2.1 监控数据流
```
┌─────────────────────────────────────────────────────────────┐
│ Admin API Layer │
│ /admin/test/* /admin/monitoring/* /admin/dashboard/* │
└────────────┬────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Monitoring Service Layer │
│ ├─ FlowTestService (完整流程测试) │
│ ├─ IntentMonitor (意图规则监控) │
│ ├─ PromptMonitor (Prompt 模板监控) │
│ ├─ FlowMonitor (话术流程监控) │
│ ├─ GuardrailMonitor (护栏监控) │
│ └─ ConversationTracker (对话追踪) │
└────────────┬────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Orchestrator (增强) │
│ 12-step pipeline + 监控埋点 (可选开关) │
└────────────┬────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Data Storage Layer │
│ ├─ PostgreSQL (统计数据、对话记录) │
│ └─ Redis (缓存、实时统计) │
└─────────────────────────────────────────────────────────────┘
```
#### 19.2.2 监控模式
**生产模式(默认)**
- 仅记录关键指标命中次数错误率平均延迟
- 不记录详细的步骤执行日志
- 性能影响 <2%
**测试模式(显式开启)**
- 记录完整的 12 步执行细节
- 包含每步的输入/输出/耗时/错误
- 仅用于 `/admin/test/*` 端点
- 数据保留 7
---
### 19.3 Dashboard 统计增强设计
#### 19.3.1 新增统计指标(对应 AC-AISVC-91, AC-AISVC-92
在现有 `GET /admin/dashboard/stats` 响应中新增以下字段
```python
# 意图规则统计
intentRuleHitRate: float # 命中率 = 命中次数 / 总对话次数
intentRuleHitCount: int # 总命中次数
intentRuleTopHits: list[dict] # Top 5 命中规则 [{"ruleId", "name", "hitCount"}]
# Prompt 模板统计
promptTemplateUsageCount: int # 模板使用总次数
promptTemplateActiveCount: int # 已发布模板数量
promptTemplateTopUsed: list[dict] # Top 5 使用模板 [{"templateId", "name", "usageCount"}]
# 话术流程统计
scriptFlowActivationCount: int # 流程激活总次数
scriptFlowCompletionRate: float # 完成率 = 完成次数 / 激活次数
scriptFlowTopActive: list[dict] # Top 5 活跃流程 [{"flowId", "name", "activationCount"}]
# 护栏统计
guardrailBlockCount: int # 拦截总次数
guardrailBlockRate: float # 拦截率 = 拦截次数 / 总对话次数
guardrailTopWords: list[dict] # Top 5 触发禁词 [{"word", "category", "hitCount"}]
```
#### 19.3.2 数据来源与计算
**实时统计Redis**
- 使用 Redis Hash 存储租户级计数器
- Key 格式`stats:{tenant_id}:{metric}:{date}`
- 每次对话结束时异步更新不阻塞响应
- TTL = 90
**聚合统计PostgreSQL**
- 从现有表的 `hit_count` 字段聚合intent_rules, forbidden_words
- `flow_instances` 表统计流程激活与完成
- `chat_messages` 表关联 `prompt_template_id`需新增字段
#### 19.3.3 性能优化
- Dashboard 统计结果缓存 60 Redis
- Top N 排行榜每 5 分钟预计算一次后台任务
- 避免实时聚合大表使用增量计数器
---
### 19.4 完整流程测试台设计
#### 19.4.1 测试接口(对应 AC-AISVC-93 ~ AC-AISVC-96
**端点**`POST /admin/test/flow-execution`
**请求体**
```json
{
"message": "我想退货",
"sessionId": "test_session_001",
"channelType": "wechat",
"history": [...], // 可选
"metadata": {...}, // 可选
"enableDetailedLog": true // 是否记录详细日志
}
```
**响应体**
```json
{
"executionId": "exec_uuid",
"steps": [
{
"step": 1,
"name": "Memory.load",
"status": "success",
"durationMs": 12,
"input": {"sessionId": "test_session_001"},
"output": {"messageCount": 5},
"error": null,
"metadata": {}
},
{
"step": 3,
"name": "InputGuardrail.scan",
"status": "success",
"durationMs": 8,
"input": {"message": "我想退货"},
"output": {"triggered": false, "words": []},
"error": null,
"metadata": {}
},
{
"step": 5,
"name": "IntentRouter.match",
"status": "success",
"durationMs": 15,
"input": {"message": "我想退货"},
"output": {
"matched": true,
"ruleId": "rule_001",
"ruleName": "退货意图",
"responseType": "flow",
"flowId": "flow_return_001"
},
"error": null,
"metadata": {"priority": 100, "matchType": "keyword"}
},
// ... 其他步骤
],
"finalResponse": {
"reply": "您好,请问您的订单号是多少?",
"confidence": 0.95,
"shouldTransfer": false
},
"totalDurationMs": 1250,
"summary": {
"successSteps": 11,
"failedSteps": 0,
"skippedSteps": 1
}
}
```
#### 19.4.2 实现策略
**Orchestrator 增强**
```python
class Orchestrator:
async def generate_with_monitoring(
self,
request: ChatRequest,
tenant_id: str,
enable_detailed_log: bool = False
) -> tuple[ChatResponse, Optional[list[StepLog]]]:
"""
增强版生成方法,支持可选的详细日志记录
"""
step_logs = [] if enable_detailed_log else None
# Step 1: Memory.load
step_start = time.time()
try:
history = await self.memory.load_history(tenant_id, request.sessionId)
if step_logs is not None:
step_logs.append(StepLog(
step=1,
name="Memory.load",
status="success",
durationMs=int((time.time() - step_start) * 1000),
input={"sessionId": request.sessionId},
output={"messageCount": len(history)}
))
except Exception as e:
if step_logs is not None:
step_logs.append(StepLog(
step=1,
name="Memory.load",
status="failed",
durationMs=int((time.time() - step_start) * 1000),
error=str(e)
))
raise
# ... 其他步骤类似
return response, step_logs
```
**测试端点实现**
```python
@router.post("/admin/test/flow-execution")
async def test_flow_execution(
request: FlowTestRequest,
tenant_id: str = Depends(get_current_tenant_id),
session: AsyncSession = Depends(get_session)
):
# 调用增强版 Orchestrator
response, step_logs = await orchestrator.generate_with_monitoring(
ChatRequest(
message=request.message,
sessionId=request.sessionId,
channelType=request.channelType,
history=request.history,
metadata=request.metadata
),
tenant_id=tenant_id,
enable_detailed_log=request.enableDetailedLog
)
# 保存测试记录(可选,用于历史查询)
if request.enableDetailedLog:
test_record = FlowTestRecord(
tenant_id=tenant_id,
session_id=request.sessionId,
steps=step_logs,
final_response=response,
created_at=datetime.utcnow()
)
await save_test_record(session, test_record)
return FlowExecutionResult(
executionId=str(uuid.uuid4()),
steps=step_logs,
finalResponse=response,
totalDurationMs=sum(s.durationMs for s in step_logs),
summary={
"successSteps": sum(1 for s in step_logs if s.status == "success"),
"failedSteps": sum(1 for s in step_logs if s.status == "failed"),
"skippedSteps": sum(1 for s in step_logs if s.status == "skipped")
}
)
```
---
### 19.5 意图规则测试与监控设计
#### 19.5.1 独立测试接口(对应 AC-AISVC-97 ~ AC-AISVC-99
**端点**`POST /admin/intent-rules/{ruleId}/test`
**请求体**
```json
{
"testMessages": [
"我想退货",
"能退款吗",
"这个产品怎么样"
]
}
```
**响应体**
```json
{
"ruleId": "rule_001",
"ruleName": "退货意图",
"results": [
{
"message": "我想退货",
"matched": true,
"matchedKeywords": ["退货"],
"matchedPatterns": [],
"matchType": "keyword",
"priority": 100,
"conflictRules": []
},
{
"message": "能退款吗",
"matched": true,
"matchedKeywords": ["退款"],
"matchedPatterns": [],
"matchType": "keyword",
"priority": 100,
"conflictRules": []
},
{
"message": "这个产品怎么样",
"matched": false,
"matchedKeywords": [],
"matchedPatterns": [],
"matchType": null,
"priority": 100,
"conflictRules": [
{
"ruleId": "rule_002",
"ruleName": "产品咨询",
"priority": 80,
"reason": "可能匹配产品咨询规则"
}
]
}
],
"summary": {
"totalTests": 3,
"matchedCount": 2,
"matchRate": 0.67
}
}
```
#### 19.5.2 冲突检测算法
```python
class IntentRuleTester:
async def test_rule(
self,
rule: IntentRule,
test_messages: list[str],
tenant_id: str
) -> IntentRuleTestResult:
"""测试意图规则并检测冲突"""
all_rules = await self.rule_service.get_rules(tenant_id)
results = []
for message in test_messages:
# 测试当前规则
matched = self._match_rule(rule, message)
# 检测冲突:查找其他也能匹配的规则
conflict_rules = []
for other_rule in all_rules:
if other_rule.id == rule.id:
continue
if self._match_rule(other_rule, message):
conflict_rules.append({
"ruleId": other_rule.id,
"ruleName": other_rule.name,
"priority": other_rule.priority,
"reason": f"同时匹配(优先级:{other_rule.priority}"
})
results.append(IntentRuleTestCase(
message=message,
matched=matched,
conflictRules=conflict_rules
))
return IntentRuleTestResult(
ruleId=rule.id,
ruleName=rule.name,
results=results,
summary={
"totalTests": len(test_messages),
"matchedCount": sum(1 for r in results if r.matched),
"matchRate": sum(1 for r in results if r.matched) / len(test_messages)
}
)
```
#### 19.5.3 监控统计接口(对应 AC-AISVC-100
**端点**`GET /admin/monitoring/intent-rules`
**查询参数**
- `startDate`: 开始日期ISO 8601
- `endDate`: 结束日期ISO 8601
- `limit`: 返回数量默认 10
**响应体**
```json
{
"totalHits": 1250,
"totalConversations": 5000,
"hitRate": 0.25,
"rules": [
{
"ruleId": "rule_001",
"ruleName": "退货意图",
"hitCount": 450,
"hitRate": 0.09,
"responseType": "flow",
"avgResponseTime": 1200,
"lastHitAt": "2026-02-27T14:30:00Z"
}
],
"timeSeriesData": [
{
"date": "2026-02-27",
"totalHits": 120,
"ruleBreakdown": {
"rule_001": 45,
"rule_002": 30,
"rule_003": 45
}
}
]
}
```
---
### 19.6 Prompt 模板测试与监控设计
#### 19.6.1 模板预览接口(对应 AC-AISVC-101
**端点**`POST /admin/prompt-templates/{templateId}/preview`
**请求体**
```json
{
"variables": {
"persona_name": "小助手",
"custom_var": "测试值"
},
"sampleHistory": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "您好,有什么可以帮您?"}
],
"sampleMessage": "我想了解产品信息"
}
```
**响应体**
```json
{
"templateId": "tpl_001",
"templateName": "默认客服人设",
"version": 3,
"renderedSystemPrompt": "你是小助手,一个专业的客服助手...\n\n[行为约束]\n1. 不允许承诺具体赔偿金额\n...",
"finalMessages": [
{
"role": "system",
"content": "你是小助手,一个专业的客服助手..."
},
{
"role": "user",
"content": "你好"
},
{
"role": "assistant",
"content": "您好,有什么可以帮您?"
},
{
"role": "user",
"content": "我想了解产品信息"
}
],
"tokenCount": {
"systemPrompt": 450,
"history": 120,
"currentMessage": 30,
"total": 600
},
"warnings": []
}
```
#### 19.6.2 模板使用统计(对应 AC-AISVC-102
**端点**`GET /admin/monitoring/prompt-templates`
**响应体**
```json
{
"totalUsage": 5000,
"templates": [
{
"templateId": "tpl_001",
"templateName": "默认客服人设",
"scene": "chat",
"usageCount": 3500,
"usageRate": 0.70,
"currentVersion": 3,
"avgTokenCount": 450,
"lastUsedAt": "2026-02-27T14:30:00Z"
}
],
"sceneBreakdown": {
"chat": 3500,
"rag_qa": 1200,
"greeting": 300
}
}
```
#### 19.6.3 实现策略
**Token 计数**
```python
import tiktoken
class PromptTemplateMonitor:
def __init__(self):
self.tokenizer = tiktoken.get_encoding("cl100k_base")
async def preview_template(
self,
template: PromptTemplate,
variables: dict,
sample_history: list[dict],
sample_message: str
) -> PromptPreviewResult:
"""预览模板渲染结果并计算 token"""
# 1. 渲染系统指令
version = await self.get_published_version(template.id)
system_prompt = self.variable_resolver.resolve(
version.system_instruction,
variables
)
# 2. 注入行为规则
behavior_rules = await self.get_behavior_rules(template.tenant_id)
if behavior_rules:
system_prompt += "\n\n[行为约束]\n" + "\n".join(
f"{i+1}. {rule.rule_text}"
for i, rule in enumerate(behavior_rules)
)
# 3. 构建完整消息列表
messages = [{"role": "system", "content": system_prompt}]
messages.extend(sample_history)
messages.append({"role": "user", "content": sample_message})
# 4. 计算 token
token_counts = {
"systemPrompt": len(self.tokenizer.encode(system_prompt)),
"history": sum(
len(self.tokenizer.encode(msg["content"]))
for msg in sample_history
),
"currentMessage": len(self.tokenizer.encode(sample_message)),
}
token_counts["total"] = sum(token_counts.values())
# 5. 检查警告
warnings = []
if token_counts["total"] > 4000:
warnings.append("总 token 数超过 4000可能影响性能")
if token_counts["systemPrompt"] > 2000:
warnings.append("系统指令过长,建议精简")
return PromptPreviewResult(
templateId=template.id,
renderedSystemPrompt=system_prompt,
finalMessages=messages,
tokenCount=token_counts,
warnings=warnings
)
```
---
### 19.7 话术流程测试与监控设计
#### 19.7.1 流程模拟测试接口(对应 AC-AISVC-103 ~ AC-AISVC-105
**端点**`POST /admin/script-flows/{flowId}/simulate`
**请求体**
```json
{
"userInputs": [
"12345678901234",
"质量问题",
"是的"
]
}
```
**响应体**
```json
{
"flowId": "flow_001",
"flowName": "退货流程",
"simulation": [
{
"stepNo": 1,
"botMessage": "您好,请问您的订单号是多少?",
"userInput": "12345678901234",
"matchedCondition": {
"type": "pattern",
"pattern": "\\d{10,}",
"gotoStep": 2
},
"nextStep": 2,
"durationMs": 50
},
{
"stepNo": 2,
"botMessage": "请问退货原因是什么?",
"userInput": "质量问题",
"matchedCondition": {
"type": "default",
"gotoStep": 3
},
"nextStep": 3,
"durationMs": 45
},
{
"stepNo": 3,
"botMessage": "已为您登记退货申请,是否需要上门取件?",
"userInput": "是的",
"matchedCondition": {
"type": "keyword",
"keywords": ["是", "需要"],
"gotoStep": 4
},
"nextStep": 4,
"durationMs": 40
}
],
"result": {
"completed": true,
"totalSteps": 3,
"totalDurationMs": 135,
"finalMessage": "好的,我们会在 24 小时内安排快递上门取件。"
},
"coverage": {
"totalSteps": 5,
"coveredSteps": 3,
"coverageRate": 0.60,
"uncoveredSteps": [4, 5]
},
"issues": [
"流程覆盖率低于 80%,建议增加测试用例"
]
}
```
#### 19.7.2 流程覆盖率分析
```python
class ScriptFlowTester:
async def simulate_flow(
self,
flow: ScriptFlow,
user_inputs: list[str]
) -> FlowSimulationResult:
"""模拟流程执行并分析覆盖率"""
simulation = []
current_step = 1
visited_steps = set()
for user_input in user_inputs:
if current_step > len(flow.steps):
break
step_def = flow.steps[current_step - 1]
visited_steps.add(current_step)
# 匹配下一步条件
matched_condition, next_step = self._match_next_step(
step_def,
user_input
)
simulation.append(FlowSimulationStep(
stepNo=current_step,
botMessage=step_def["content"],
userInput=user_input,
matchedCondition=matched_condition,
nextStep=next_step
))
current_step = next_step
# 分析覆盖率
total_steps = len(flow.steps)
covered_steps = len(visited_steps)
coverage_rate = covered_steps / total_steps
# 检测问题
issues = []
if coverage_rate < 0.8:
issues.append("流程覆盖率低于 80%,建议增加测试用例")
# 检测死循环
if len(simulation) > total_steps * 2:
issues.append("检测到可能的死循环")
# 检测未覆盖的分支
uncovered_steps = set(range(1, total_steps + 1)) - visited_steps
if uncovered_steps:
issues.append(f"未覆盖步骤:{sorted(uncovered_steps)}")
return FlowSimulationResult(
flowId=flow.id,
simulation=simulation,
coverage={
"totalSteps": total_steps,
"coveredSteps": covered_steps,
"coverageRate": coverage_rate,
"uncoveredSteps": list(uncovered_steps)
},
issues=issues
)
```
#### 19.7.3 流程监控统计(对应 AC-AISVC-106
**端点**`GET /admin/monitoring/script-flows`
**响应体**
```json
{
"totalActivations": 850,
"totalCompletions": 680,
"completionRate": 0.80,
"flows": [
{
"flowId": "flow_001",
"flowName": "退货流程",
"activationCount": 450,
"completionCount": 380,
"completionRate": 0.84,
"avgDuration": 180,
"avgStepsCompleted": 4.2,
"dropOffPoints": [
{
"stepNo": 2,
"dropOffCount": 50,
"dropOffRate": 0.11
}
],
"lastActivatedAt": "2026-02-27T14:30:00Z"
}
]
}
```
---
### 19.8 输出护栏测试与监控设计
#### 19.8.1 禁词测试接口(对应 AC-AISVC-107
**端点**`POST /admin/guardrails/forbidden-words/test`
**请求体**
```json
{
"testTexts": [
"我们的产品比竞品 A 更好",
"可以给您赔偿 1000 元",
"这是正常的回复"
]
}
```
**响应体**
```json
{
"results": [
{
"originalText": "我们的产品比竞品 A 更好",
"triggered": true,
"triggeredWords": [
{
"word": "竞品 A",
"category": "competitor",
"strategy": "replace",
"replacement": "其他品牌"
}
],
"filteredText": "我们的产品比其他品牌更好",
"blocked": false
},
{
"originalText": "可以给您赔偿 1000 元",
"triggered": true,
"triggeredWords": [
{
"word": "赔偿",
"category": "sensitive",
"strategy": "block",
"fallbackReply": "关于补偿问题,请联系人工客服处理"
}
],
"filteredText": "关于补偿问题,请联系人工客服处理",
"blocked": true
},
{
"originalText": "这是正常的回复",
"triggered": false,
"triggeredWords": [],
"filteredText": "这是正常的回复",
"blocked": false
}
],
"summary": {
"totalTests": 3,
"triggeredCount": 2,
"blockedCount": 1,
"triggerRate": 0.67
}
}
```
#### 19.8.2 护栏监控统计(对应 AC-AISVC-108
**端点**`GET /admin/monitoring/guardrails`
**响应体**
```json
{
"totalBlocks": 120,
"totalTriggers": 450,
"blockRate": 0.024,
"words": [
{
"wordId": "word_001",
"word": "竞品 A",
"category": "competitor",
"strategy": "replace",
"hitCount": 85,
"blockCount": 0,
"lastHitAt": "2026-02-27T14:30:00Z"
},
{
"wordId": "word_002",
"word": "赔偿",
"category": "sensitive",
"strategy": "block",
"hitCount": 45,
"blockCount": 45,
"lastHitAt": "2026-02-27T14:25:00Z"
}
],
"categoryBreakdown": {
"competitor": 85,
"sensitive": 45,
"political": 0,
"custom": 20
}
}
```
---
### 19.9 对话追踪与导出设计
#### 19.9.1 对话追踪接口(对应 AC-AISVC-109
**端点**`GET /admin/monitoring/conversations`
**查询参数**
- `startDate`: 开始日期ISO 8601
- `endDate`: 结束日期ISO 8601
- `sessionId`: 会话 ID可选
- `channelType`: 渠道类型可选
- `hasError`: 是否包含错误可选
- `limit`: 返回数量默认 20
- `offset`: 偏移量默认 0
**响应体**
```json
{
"total": 1250,
"conversations": [
{
"sessionId": "kf_001_wx123",
"channelType": "wechat",
"messageCount": 12,
"startTime": "2026-02-27T14:00:00Z",
"lastMessageTime": "2026-02-27T14:15:00Z",
"duration": 900,
"intentRulesHit": [
{"ruleId": "rule_001", "ruleName": "退货意图", "hitCount": 2}
],
"flowsActivated": [
{"flowId": "flow_001", "flowName": "退货流程", "status": "completed"}
],
"guardrailTriggered": true,
"errorCount": 0,
"avgConfidence": 0.85,
"transferRequested": false
}
]
}
```
#### 19.9.2 对话详情接口
**端点**`GET /admin/monitoring/conversations/{sessionId}`
**响应体**
```json
{
"sessionId": "kf_001_wx123",
"channelType": "wechat",
"startTime": "2026-02-27T14:00:00Z",
"messages": [
{
"messageId": "msg_001",
"role": "user",
"content": "我想退货",
"timestamp": "2026-02-27T14:00:00Z"
},
{
"messageId": "msg_002",
"role": "assistant",
"content": "您好,请问您的订单号是多少?",
"timestamp": "2026-02-27T14:00:02Z",
"confidence": 0.95,
"intentMatched": {
"ruleId": "rule_001",
"ruleName": "退货意图",
"responseType": "flow"
},
"flowActivated": {
"flowId": "flow_001",
"flowName": "退货流程",
"currentStep": 1
},
"guardrailResult": {
"triggered": false,
"words": []
},
"latencyMs": 1200,
"totalTokens": 450,
"promptTokens": 380,
"completionTokens": 70
}
],
"summary": {
"totalMessages": 12,
"userMessages": 6,
"assistantMessages": 6,
"avgConfidence": 0.85,
"avgLatency": 1150,
"totalTokens": 5400,
"intentRulesHit": 2,
"flowsActivated": 1,
"guardrailTriggered": false,
"errorOccurred": false
}
}
```
#### 19.9.3 对话导出接口(对应 AC-AISVC-110
**端点**`POST /admin/monitoring/conversations/export`
**请求体**
```json
{
"startDate": "2026-02-20T00:00:00Z",
"endDate": "2026-02-27T23:59:59Z",
"format": "csv",
"filters": {
"channelType": "wechat",
"hasError": false,
"minConfidence": 0.7
},
"fields": [
"sessionId",
"channelType",
"messageCount",
"avgConfidence",
"intentRulesHit",
"flowsActivated"
]
}
```
**响应体**
```json
{
"exportId": "export_uuid",
"status": "processing",
"estimatedRows": 1250,
"downloadUrl": null,
"expiresAt": null
}
```
**导出状态查询**`GET /admin/monitoring/conversations/export/{exportId}`
**响应体**
```json
{
"exportId": "export_uuid",
"status": "completed",
"totalRows": 1250,
"downloadUrl": "/admin/monitoring/conversations/export/export_uuid/download",
"expiresAt": "2026-02-28T14:30:00Z",
"createdAt": "2026-02-27T14:25:00Z"
}
```
#### 19.9.4 实现策略
**异步导出处理**
```python
import asyncio
import csv
from io import StringIO
class ConversationExporter:
async def export_conversations(
self,
tenant_id: str,
filters: dict,
fields: list[str],
format: str = "csv"
) -> str:
"""异步导出对话数据"""
export_id = str(uuid.uuid4())
# 创建导出任务记录
export_task = ExportTask(
id=export_id,
tenant_id=tenant_id,
status="processing",
created_at=datetime.utcnow()
)
await self.save_export_task(export_task)
# 异步执行导出
asyncio.create_task(self._process_export(
export_id,
tenant_id,
filters,
fields,
format
))
return export_id
async def _process_export(
self,
export_id: str,
tenant_id: str,
filters: dict,
fields: list[str],
format: str
):
"""后台处理导出任务"""
try:
# 1. 查询对话数据(分批处理,避免内存溢出)
batch_size = 1000
offset = 0
output = StringIO()
writer = csv.DictWriter(output, fieldnames=fields)
writer.writeheader()
while True:
conversations = await self.query_conversations(
tenant_id,
filters,
limit=batch_size,
offset=offset
)
if not conversations:
break
for conv in conversations:
row = {field: conv.get(field) for field in fields}
writer.writerow(row)
offset += batch_size
# 2. 保存到临时文件
file_path = f"/tmp/exports/{export_id}.csv"
with open(file_path, "w", encoding="utf-8") as f:
f.write(output.getvalue())
# 3. 更新导出任务状态
await self.update_export_task(
export_id,
status="completed",
file_path=file_path,
total_rows=offset,
expires_at=datetime.utcnow() + timedelta(hours=24)
)
except Exception as e:
logger.error(f"Export failed: {e}")
await self.update_export_task(
export_id,
status="failed",
error=str(e)
)
```
---
### 19.10 新增数据库实体汇总
v0.7.0 新增以下 SQLModel 实体均包含 `tenant_id` 字段遵循现有多租户隔离模式
| 实体 | 表名 | 用途 | 关键字段 |
|------|------|------|----------|
| FlowTestRecord | flow_test_records | 完整流程测试记录 | session_id, steps (JSONB), final_response (JSONB) |
| ExportTask | export_tasks | 对话导出任务 | status, file_path, total_rows, expires_at |
**扩展现有实体**
| 实体 | 表名 | 新增字段 | 用途 |
|------|------|----------|------|
| ChatMessage | chat_messages | prompt_template_id (UUID, nullable) | 关联使用的 Prompt 模板 |
| ChatMessage | chat_messages | intent_rule_id (UUID, nullable) | 关联命中的意图规则 |
| ChatMessage | chat_messages | flow_instance_id (UUID, nullable) | 关联的话术流程实例 |
| ChatMessage | chat_messages | guardrail_triggered (BOOLEAN) | 是否触发护栏 |
| ChatMessage | chat_messages | guardrail_words (JSONB, nullable) | 触发的禁词列表 |
**索引优化**
```sql
-- 监控查询优化
CREATE INDEX idx_chat_messages_monitoring
ON chat_messages(tenant_id, created_at DESC, role);
-- 意图规则统计优化
CREATE INDEX idx_chat_messages_intent
ON chat_messages(tenant_id, intent_rule_id)
WHERE intent_rule_id IS NOT NULL;
-- 流程统计优化
CREATE INDEX idx_flow_instances_monitoring
ON flow_instances(tenant_id, status, started_at DESC);
-- 护栏统计优化
CREATE INDEX idx_chat_messages_guardrail
ON chat_messages(tenant_id, guardrail_triggered, created_at DESC)
WHERE guardrail_triggered = true;
```
---
### 19.11 性能优化策略
#### 19.11.1 缓存策略
**Redis 缓存层次**
```
Level 1: Dashboard 统计TTL 60s
- Key: stats:{tenant_id}:dashboard
- 内容:聚合统计数据
Level 2: Top N 排行榜TTL 300s
- Key: stats:{tenant_id}:top:intent_rules
- Key: stats:{tenant_id}:top:prompt_templates
- Key: stats:{tenant_id}:top:script_flows
- Key: stats:{tenant_id}:top:forbidden_words
Level 3: 实时计数器TTL 90天
- Key: stats:{tenant_id}:counter:{metric}:{date}
- 内容:增量计数器
```
**缓存更新策略**
- Dashboard 统计每次对话结束后异步更新计数器
- Top N 排行榜后台任务每 5 分钟重新计算
- 实时计数器使用 Redis INCR 原子操作
#### 19.11.2 数据库优化
**分区策略**
```sql
-- flow_test_records 按日期分区(保留 7 天)
CREATE TABLE flow_test_records (
id UUID PRIMARY KEY,
tenant_id VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL,
...
) PARTITION BY RANGE (created_at);
CREATE TABLE flow_test_records_2026_02_27
PARTITION OF flow_test_records
FOR VALUES FROM ('2026-02-27') TO ('2026-02-28');
-- 自动清理过期分区(定时任务)
DROP TABLE IF EXISTS flow_test_records_2026_02_20;
```
**查询优化**
- 使用覆盖索引减少回表查询
- 对大表使用 LIMIT + 游标分页
- 避免 SELECT *只查询需要的字段
- 使用 EXPLAIN ANALYZE 分析慢查询
#### 19.11.3 监控埋点优化
**最小化性能影响**
```python
class MonitoringMiddleware:
async def __call__(self, request, call_next):
# 仅在测试模式或采样时记录详细日志
enable_detailed_log = (
request.url.path.startswith("/admin/test/") or
self._should_sample() # 1% 采样率
)
if enable_detailed_log:
# 记录详细步骤日志
request.state.monitoring_enabled = True
response = await call_next(request)
# 异步更新统计(不阻塞响应)
if hasattr(request.state, "monitoring_data"):
asyncio.create_task(
self._update_stats_async(request.state.monitoring_data)
)
return response
def _should_sample(self) -> bool:
"""1% 采样率"""
return random.random() < 0.01
```
---
### 19.12 v0.7.0 风险与待澄清
#### 19.12.1 性能风险
- **完整流程测试**记录 12 步详细日志会增加 10-15% 的延迟仅用于测试环境
- **对话导出**大批量导出>10000 条)可能导致内存压力,需要流式处理。
- **实时统计**:高并发场景下 Redis 计数器可能成为瓶颈,考虑使用 Redis Cluster。
#### 19.12.2 存储风险
- **测试日志膨胀**:完整流程测试日志每条约 5KB需严格执行 7 天清理策略。
- **导出文件管理**导出文件需要定期清理24 小时过期),避免磁盘占用。
- **索引膨胀**:新增多个索引会增加写入开销,需监控索引使用率。
#### 19.12.3 功能待澄清
- **对话导出格式**:是否需要支持 JSON/Excel 格式?当前仅实现 CSV。
- **实时监控推送**:是否需要 WebSocket 实时推送监控数据?当前仅支持轮询。
- **历史数据迁移**:现有对话数据是否需要回填 `prompt_template_id` 等新字段?
- **权限控制**:测试与监控接口是否需要更细粒度的权限控制(如只读/读写)?
#### 19.12.4 兼容性风险
- **数据库迁移**:新增字段和索引需要在生产环境谨慎执行,建议分批迁移。
- **API 版本**:新增监控接口不影响现有 `/ai/chat` 接口,向后兼容。
- **前端适配**Dashboard 新增统计字段需要前端同步更新,否则显示为空。
---
## 20. 总结
v0.7.0 测试与监控系统为 AI 中台提供了完整的可观测性与可测试性能力:
**核心价值**
- **独立测试**为四大功能Prompt 模板、意图规则、话术流程、输出护栏)提供独立测试能力
- **完整追踪**12 步流程的详细执行日志,支持问题排查与效果评估
- **实时监控**:细粒度的运行时统计,支持规则命中率、流程完成率、护栏拦截率等
- **数据导出**:支持对话数据导出,便于离线分析与模型优化
**技术亮点**
- **性能优先**:生产模式性能影响 <2%测试模式仅在显式开启时生效
- **存储可控**测试日志 7 天自动清理导出文件 24 小时过期
- **租户隔离**所有监控数据按 `tenant_id` 隔离保证多租户安全
- **向后兼容**新增监控不影响现有接口行为与性能
**实施建议**
1. 优先实现 Dashboard 统计增强AC-AISVC-91, AC-AISVC-92
2. 其次实现完整流程测试台AC-AISVC-93 ~ AC-AISVC-96
3. 再实现各功能的独立测试接口AC-AISVC-97 ~ AC-AISVC-108
4. 最后实现对话追踪与导出AC-AISVC-109, AC-AISVC-110