69 KiB
| feature_id | title | status | version | last_updated | inputs | ||||
|---|---|---|---|---|---|---|---|---|---|
| AISVC | Python AI 中台(ai-service)技术设计 | draft | 0.7.0 | 2026-02-27 |
|
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事件序列。
- non-streaming 响应字段必须包含
- 实现 AI 侧会话记忆:基于
(tenantId, sessionId)持久化与加载。 - 实现 RAG(MVP:向量检索)并预留图谱检索(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 分层概览
本服务按“职责单一 + 可插拔”的原则分为五层:
- API 层(Transport / Controller)
- 职责:
- HTTP 请求解析、参数校验(含
X-Tenant-Id)、鉴权/限流(如后续需要)。 - 根据
Accept头选择 non-streaming 或 SSE streaming。 - 统一错误映射为
ErrorResponse。
- HTTP 请求解析、参数校验(含
- 输入:
X-Tenant-Idheader +ChatRequestbody。 - 输出:
- JSON:
ChatResponse - SSE:
message/final/error事件流。
- JSON:
- 编排层(Orchestrator / Use Case)
- 职责:
- 整体流程编排:加载会话记忆 → 合并上下文 →(可选)RAG 检索 → 组装 prompt → 调用 LLM → 计算置信度与转人工建议 → 写回记忆。
- 在 streaming 模式下,将 LLM 的增量输出转为 SSE
message事件,同时维护最终reply。
- 输入:
tenantId, sessionId, currentMessage, channelType, history?, metadata? - 输出:
- non-streaming:一次性
ChatResponse - streaming:增量 token(或片段)流 + 最终
ChatResponse。
- non-streaming:一次性
- 记忆层(Memory)
- 职责:
- 持久化会话消息与摘要/记忆(最小:消息列表)。
- 提供按
(tenantId, sessionId)查询的会话上下文读取 API。
- 存储:PostgreSQL。
- 检索层(Retrieval)
- 职责:
- 提供统一
Retriever抽象接口。 - MVP 实现:向量检索(Qdrant)。
- 插件点:图谱检索(Neo4j)实现可新增而不改动 Orchestrator。
- 提供统一
- LLM 适配层(LLM Adapter)
- 职责:
- 屏蔽不同 LLM 提供方差异(请求格式、流式回调、重试策略)。
- 提供:一次性生成接口 + 流式生成接口(yield token/delta)。
2.2 关键数据流(文字版)
- API 层接收请求 → 提取
tenantId(Header)与 body → 调用 Orchestrator。 - Orchestrator:
- Memory.load(tenantId, sessionId)
- merge_context(local_history, external_history)
- Retrieval.retrieve(query, tenantId, channelType, metadata)(MVP 向量检索)
- build_prompt(merged_history, retrieved_docs, currentMessage)
- LLM.generate(...)(non-streaming)或 LLM.stream_generate(...)(streaming)
- compute_confidence(…)
- Memory.append(tenantId, sessionId, user/assistant messages)
- 返回
ChatResponse(或通过 SSE 输出)。
3. API 与协议设计要点
3.1 tenantId 放置与处理
- 主入口:
X-Tenant-Idheader(契约已声明 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 步骤
- Query 规范化
- 输入:
currentMessage(可结合channelType与 metadata)。 - 规则:去噪、截断(防止超长)、可选的 query rewrite(MVP 可不做)。
- Embedding
- 由
EmbeddingProvider生成向量(可复用 LLM 适配层或独立适配层)。 - ✅ 已确认:Token 计数统一使用
tiktoken进行精确计算(用于 history 截断与证据预算)。
- 向量检索(Qdrant)
- 按租户隔离选择 collection(见 5.1)。
- 使用 topK + score threshold 过滤。
- 上下文构建
- 将检索结果转为 “证据片段列表”,限制总 token 与片段数。
- 生成 prompt 时区分:系统指令 / 对话历史 / 证据 / 当前问题。
- 生成与引用策略
- 生成回答必须优先依据证据。
- 若证据不足:触发兜底策略(见 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 超限导致可用证据过少。
兜底动作:
- 回复策略:
- 明确表达“未从知识库确认/建议咨询人工/提供可执行下一步”。
- 避免编造具体事实性结论。
- 置信度:
- 以
T_low为阈值(可配置),检索不足场景通常产生较低confidence。
- 转人工建议:
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_sessionstenant_id(NOT NULL)session_id(NOT NULL)created_at,updated_at- 主键/唯一约束:
(tenant_id, session_id)
-
chat_messagestenant_id(NOT NULL)session_id(NOT NULL)message_id(UUID 或 bigserial)role(user/assistant)content(text)created_at
5.2.2 分区策略
根据租户规模选择:
方案 A(MVP 推荐):逻辑分区 + 复合索引
- 不做 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 Security(RLS)并通过
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。
- 每个 delta 输出前检查状态必须为
6.3 异常处理
-
参数错误:在进入流式生成前即可判定,直接发送
error(或返回 400,取决于是否已经选择 SSE;建议 SSE 模式同样用event:error输出 ErrorResponse)。 -
下游依赖错误(LLM/Qdrant/PG):
- 若尚未开始输出:可直接返回 503/500 JSON(non-streaming)或发送
event:error(streaming)。 - 若已输出部分
message:必须以event:error收尾。
- 若尚未开始输出:可直接返回 503/500 JSON(non-streaming)或发送
-
客户端断开:
- 立即停止 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))- normalized:trim + 统一空白(MVP 简化:trim)。
去重策略:
- 先以
H_local构建seen集合。 - 遍历
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 依赖接口(概念)
MemoryStoreload_history(tenantId, sessionId) -> messages[]append_messages(tenantId, sessionId, messages[])
Retrieverretrieve(tenantId, query, metadata) -> RetrievalResult
LLMClientgenerate(prompt, params) -> textstream_generate(prompt, params) -> iterator[delta]
8.2 插件点
- Retrieval:VectorRetriever / GraphRetriever / HybridRetriever
- LLM:OpenAICompatibleClient / 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}} |
渠道类型 | |
{{tenant_name}} |
租户名称 | 某某公司 |
{{session_id}} |
会话ID | kf_001_wx123 |
自定义变量:由模板定义,管理员在模板中声明变量名和默认值。
替换流程:
- 加载已发布版本的
system_instruction - 合并内置变量 + 自定义变量默认值
- 执行
{{variable}}模式替换 - 注入行为规则(从 guardrails 加载,追加到系统指令末尾)
- 输出最终 system message
11.3 缓存策略
- 使用内存缓存(dict),key =
(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 匹配算法
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 核心逻辑
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 输出过滤流程
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 改写
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 分层排序
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 响应中新增以下字段:
# 意图规则统计
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
请求体:
{
"message": "我想退货",
"sessionId": "test_session_001",
"channelType": "wechat",
"history": [...], // 可选
"metadata": {...}, // 可选
"enableDetailedLog": true // 是否记录详细日志
}
响应体:
{
"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 增强:
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
测试端点实现:
@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
请求体:
{
"testMessages": [
"我想退货",
"能退款吗",
"这个产品怎么样"
]
}
响应体:
{
"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 冲突检测算法
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)
响应体:
{
"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
请求体:
{
"variables": {
"persona_name": "小助手",
"custom_var": "测试值"
},
"sampleHistory": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "您好,有什么可以帮您?"}
],
"sampleMessage": "我想了解产品信息"
}
响应体:
{
"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
响应体:
{
"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 计数:
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
请求体:
{
"userInputs": [
"12345678901234",
"质量问题",
"是的"
]
}
响应体:
{
"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 流程覆盖率分析
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
响应体:
{
"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
请求体:
{
"testTexts": [
"我们的产品比竞品 A 更好",
"可以给您赔偿 1000 元",
"这是正常的回复"
]
}
响应体:
{
"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
响应体:
{
"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)
响应体:
{
"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}
响应体:
{
"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
请求体:
{
"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"
]
}
响应体:
{
"exportId": "export_uuid",
"status": "processing",
"estimatedRows": 1250,
"downloadUrl": null,
"expiresAt": null
}
导出状态查询:GET /admin/monitoring/conversations/export/{exportId}
响应体:
{
"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 实现策略
异步导出处理:
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) | 触发的禁词列表 |
索引优化:
-- 监控查询优化
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 数据库优化
分区策略:
-- 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 监控埋点优化
最小化性能影响:
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隔离,保证多租户安全 - 向后兼容:新增监控不影响现有接口行为与性能
实施建议:
- 优先实现 Dashboard 统计增强(AC-AISVC-91, AC-AISVC-92)
- 其次实现完整流程测试台(AC-AISVC-93 ~ AC-AISVC-96)
- 再实现各功能的独立测试接口(AC-AISVC-97 ~ AC-AISVC-108)
- 最后实现对话追踪与导出(AC-AISVC-109, AC-AISVC-110)