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

69 KiB
Raw Blame History

feature_id title status version last_updated inputs
AISVC Python AI 中台ai-service技术设计 draft 0.7.0 2026-02-27
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/chatnon-streaming JSONSSE streaming 两种返回模式,并确保与契约一致:
    • non-streaming 响应字段必须包含 reply/confidence/shouldTransfer
    • streaming 通过 Accept: text/event-stream 输出 message/final/error 事件序列。
  • 实现 AI 侧会话记忆:基于 (tenantId, sessionId) 持久化与加载。
  • 实现 RAGMVP向量检索并预留图谱检索Neo4j插件点。
  • 多租户隔离:
    • Qdrant一租户一 collection或一租户一 collection 前缀)。
    • PostgreSQLtenant_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。
  • 输出:
    • JSONChatResponse
    • SSEmessage/final/error 事件流。
  1. 编排层Orchestrator / Use Case
  • 职责:
    • 整体流程编排:加载会话记忆 → 合并上下文 →可选RAG 检索 → 组装 prompt → 调用 LLM → 计算置信度与转人工建议 → 写回记忆。
    • 在 streaming 模式下,将 LLM 的增量输出转为 SSE message 事件,同时维护最终 reply
  • 输入:tenantId, sessionId, currentMessage, channelType, history?, metadata?
  • 输出:
    • non-streaming一次性 ChatResponse
    • streaming增量 token或片段流 + 最终 ChatResponse
  1. 记忆层Memory
  • 职责:
    • 持久化会话消息与摘要/记忆(最小:消息列表)。
    • 提供按 (tenantId, sessionId) 查询的会话上下文读取 API。
  • 存储PostgreSQL。
  1. 检索层Retrieval
  • 职责:
    • 提供统一 Retriever 抽象接口。
    • MVP 实现向量检索Qdrant
    • 插件点图谱检索Neo4j实现可新增而不改动 Orchestrator。
  1. LLM 适配层LLM Adapter
  • 职责:
    • 屏蔽不同 LLM 提供方差异(请求格式、流式回调、重试策略)。
    • 提供:一次性生成接口 + 流式生成接口yield token/delta

2.2 关键数据流(文字版)

  • API 层接收请求 → 提取 tenantIdHeader与 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 可不做)。
  1. Embedding
  • EmbeddingProvider 生成向量(可复用 LLM 适配层或独立适配层)。
  • 已确认Token 计数统一使用 tiktoken 进行精确计算(用于 history 截断与证据预算)。
  1. 向量检索Qdrant
  • 按租户隔离选择 collection见 5.1)。
  • 使用 topK + score threshold 过滤。
  1. 上下文构建
  • 将检索结果转为 “证据片段列表”,限制总 token 与片段数。
  • 生成 prompt 时区分:系统指令 / 对话历史 / 证据 / 当前问题。
  1. 生成与引用策略
  • 生成回答必须优先依据证据。
  • 若证据不足:触发兜底策略(见 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 < minHitsmax(score) < scoreThreshold 或 evidence token 超限导致可用证据过少。

兜底动作:

  1. 回复策略:
  • 明确表达“未从知识库确认/建议咨询人工/提供可执行下一步”。
  • 避免编造具体事实性结论。
  1. 置信度:
  • T_low 为阈值(可配置),检索不足场景通常产生较低 confidence
  1. 转人工建议:
  • confidence < T_lowshouldTransfer=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_messagestenant_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
  • 禁止同时发送 finalerror

实现策略(概念):

  • 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:errorstreaming
    • 若已输出部分 message:必须以 event:error 收尾。
  • 客户端断开:

    • 立即停止 LLM 流(如果适配层支持 cancel并避免继续写入 response。
  • 已确认:必须实现 SSE 心跳Keep-alive以注释行形式定期发送 : ping(不改变事件模型),防止网关/中间件断开连接。

  • 已确认Python 内部设置 20s 硬超时(包含 LLM 调用与检索/存储等关键步骤的总体超时控制),防止资源泄露与请求堆积。


7. 上下文合并规则Java history + 本地持久化 history

7.1 合并输入

  • H_localMemory 层基于 (tenantId, sessionId) 读取到的历史(按时间排序)。
  • H_extJava 请求中可选的 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_extH_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 匹配算法

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 隔离,保证多租户安全
  • 向后兼容:新增监控不影响现有接口行为与性能

实施建议

  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