Files
kbqa-system/docs/superpowers/specs/2026-07-13-kbqa-system-design.md
T
2026-07-13 11:18:02 +08:00

552 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 双 Agent 个人知识库问答系统设计
**状态:** 已确认
**日期:** 2026-07-13
**设计范围:** 首个可运行、可持久化、可真实模型验收的本地单用户版本
## 1. 目标
构建一个本地单用户知识库问答系统。用户可以上传可提取文本的 PDF、Markdown 和 TXT 文件;系统在后台调用真实 Embedding 模型完成分块和向量索引;用户通过双 Agent 对知识库进行严格、有引用、可多轮追问的问答,并通过 SSE 接收流式回答。
首版完成标准是打通并验证以下完整链路:
> 上传真实文档 → 后台真实 Embedding 索引 → 发起问题 → 双 Agent 真实检索和生成 → SSE 流式回答 → 右栏展示可点击引用 → 多轮追问 → 重启后文档与会话仍存在 → 删除文档/会话后相关数据完整清理
## 2. 已确认约束
- 产品形态是本机运行的单用户个人知识库,不实现登录、权限和用户隔离。
- 关系数据库固定使用 SQLite,不使用 PostgreSQL、psycopg 或 JSONB。
- 向量数据库固定使用 Milvus Lite。
- Agent 框架固定使用 LangChain 1.x API。
- Agent 必须是双 Agent:主 Agent 负责组织回答,研究 Agent 负责规划并执行一次或多次检索。
- Agent 创建使用 `langchain.agents.create_agent`,不使用旧的 `langgraph.prebuilt.create_react_agent`
- 主 Agent 与研究 Agent 均使用 `qwen3.5-flash`
- Embedding 使用 `text-embedding-v4`,向量维度固定为 `1024`
- Chat 与 Embedding 均通过 `.env` 中配置的 DashScope OpenAI 兼容地址和密钥真实调用。
- 系统采用严格知识库模式:没有足够证据时明确回答资料不足,不使用模型自身知识补全事实。
- PDF 只支持可提取文本的文件,不实现扫描件 OCR。
- 文档上传后后台异步索引,接口不等待 Embedding 完成。
- 应用重启后必须恢复未完成的索引任务。
- SQLite 保存历史会话、用户消息、最终回答和引用;Agent 内部消息不写入业务数据库。
- LangSmith 必须接入,用于记录主 Agent、研究 Agent、工具调用和耗时。
- LangSmith 使用官方 `LANGSMITH_` 环境变量;其余应用配置使用 `KBQA_` 前缀。
- 首版本机直接运行,不提供 Docker 或 Docker Compose。
- Python 项目使用 `uv`,提交 `.python-version` 固定 Python 3.12。
- Node.js 项目使用 `npm`
- 不采用 TDD。测试在功能实现后编写和执行。
- 模型和 Embedding 测试不得使用 Mock`live` 测试直接调用真实服务。
- 前端必须使用 `agent-browser` 操作真实页面完成端到端验收。
## 3. 范围
### 3.1 首版功能
- 上传、列出、查看、重试和删除 PDF、Markdown、TXT 文档。
- 查看文档索引状态、失败原因和分块原文。
- 创建、列出和删除会话。
- 查看持久化的历史消息与仍然有效的引用。
- 通过 POST SSE 接口提交问题并接收研究状态、引用、回答 token 和结束/错误事件。
- 支持依赖历史语境的多轮追问。
- 显示本轮引用来源、相关度和完整 chunk 原文。
- 提供 SQLite 与 Milvus Lite 健康检查。
- 通过 LangSmith 查看双 Agent 的完整运行轨迹。
### 3.2 明确不做
- 用户账号、鉴权、角色权限或租户隔离。
- 扫描 PDF OCR。
- WebSocket。
- Docker 化和远程生产部署。
- 多进程 Uvicorn worker。
- 混合检索、关键词检索、重排模型或知识图谱。
- 会话自动摘要和无限上下文记忆。
- 主 Agent 使用模型自身知识回答资料外问题。
- 保存子 Agent 工具消息或内部执行状态到 SQLite。
- 模型、Embedding 或向量数据库的测试替身。
## 4. 总体架构
项目采用单仓库:
```text
浏览器中的 React 三栏工作台
├─ REST:文档、会话、历史和健康检查
└─ POST + ReadableStreamSSE 流式问答
单进程 FastAPI 应用
├─ API 路由与统一错误处理
├─ 文档、会话和问答应用服务
├─ SQLite 持久任务 + asyncio 单消费者索引 Worker
└─ LangChain 1.x 双 Agent
├─ 主 Agentresearch 工具
└─ 研究 Agentsearch_knowledge_base 工具
┌──────────┼──────────┐
▼ ▼ ▼
SQLite Milvus Lite data/raw
├─ DashScope:真实 Chat 与 Embedding
└─ LangSmithAgent 与工具追踪
```
SQLite 是业务事实源。Milvus Lite 只负责向量检索。两者无法组成跨库事务,因此通过文档状态、持久索引任务、确定性 chunk ID、幂等 upsert、按文档清理和启动恢复实现最终一致。
FastAPI 只能以一个 Uvicorn worker 运行。多进程会重复启动本地索引消费者,也会增加 SQLite 与 Milvus Lite 文件竞争。
## 5. 组件边界
### 5.1 后端
后端位于 `backend/`Python 包位于 `backend/src/kbqa/`
- `main.py`:创建 FastAPI 应用,注册中间件、路由、异常处理与生命周期。
- `config.py`:使用 `pydantic-settings` 读取 `KBQA_` 配置并校验必填项。
- `database.py`:创建 SQLAlchemy 2.x async engine、session factory,并设置 SQLite PRAGMA。
- `api/`:请求 ID、中间件、错误响应和路由汇总。
- `documents/`:文档 ORM、DTO、repository、上传/删除服务和 HTTP 路由。
- `indexing/`:文件加载、文本分割、持久任务队列、单消费者 worker 和恢复逻辑。
- `rag/`:真实 Embedding 工厂、MilvusClient 封装和检索器。
- `chat/`:会话/消息 ORM、DTO、repository、SSE 编码和问答应用服务。
- `agents/`:LLM 工厂、提示词、检索工具、研究 Agent、主 Agent 和 middleware。
- `health/`SQLite 与 Milvus Lite 探测。
每个模块只通过公开 service/repository 接口协作。HTTP 路由不直接操作 ORM、Milvus 或 AgentAgent 工具不直接管理 HTTP 响应或业务事务。
### 5.2 前端
前端位于 `web/`,采用 React + TypeScript + Vite、React Router、zustand、Tailwind CSS 与 shadcn/ui。
- `app/`:路由、全局布局和 provider。
- `features/chat/`:会话列表、消息列表、输入框、SSE client、证据栏和会话 store。
- `features/documents/`:上传、列表、状态轮询、重试、删除、分块详情。
- `components/`:只存放跨功能复用的展示组件。
- `lib/api/`:普通 JSON client、统一错误解析和请求类型。
- `lib/sse/`:可处理跨 chunk 边界的 SSE 增量解析器。
服务端数据由 API 获取;zustand 只保存当前会话、流式临时状态和界面状态,不作为持久化事实源。
## 6. SQLite 数据模型
数据库 URL 使用 `sqlite+aiosqlite:///...`。连接时启用:
- `PRAGMA foreign_keys=ON`
- `PRAGMA journal_mode=WAL`
- `PRAGMA busy_timeout=5000`
所有 ID 使用 UUID 字符串,时间统一保存为 UTC。表结构通过 Alembic 管理。
### 6.1 `documents`
| 字段 | 类型 | 约束/用途 |
|---|---|---|
| `id` | TEXT | 主键,UUID |
| `filename` | TEXT | 用户原始文件名 |
| `stored_path` | TEXT | `data/raw/{document_id}.{ext}` |
| `file_type` | TEXT | `pdf/md/txt` |
| `mime_type` | TEXT | 检测后的 MIME |
| `file_size` | INTEGER | 字节数 |
| `sha256` | TEXT | 内容完整性标识,不设置唯一约束 |
| `status` | TEXT | `pending/indexing/indexed/failed/deleting` |
| `chunk_count` | INTEGER | 默认 0 |
| `error_code` | TEXT NULL | 安全错误码 |
| `error_message` | TEXT NULL | 可展示错误摘要 |
| `created_at` | DATETIME | UTC |
| `updated_at` | DATETIME | UTC |
`status``updated_at` 建索引。
### 6.2 `index_jobs`
| 字段 | 类型 | 约束/用途 |
|---|---|---|
| `id` | TEXT | 主键,UUID |
| `document_id` | TEXT | 外键,删除文档时级联;唯一 |
| `status` | TEXT | `queued/running/succeeded/failed` |
| `attempt_count` | INTEGER | 实际执行次数 |
| `last_error` | TEXT NULL | 内部错误摘要,不包含密钥 |
| `created_at` | DATETIME | UTC |
| `started_at` | DATETIME NULL | UTC |
| `finished_at` | DATETIME NULL | UTC |
启动时将 `queued/running` 任务统一恢复为 `queued` 并重新入队。失败文档通过重试接口复用同一任务记录、增加 `attempt_count`
### 6.3 `chunks`
| 字段 | 类型 | 约束/用途 |
|---|---|---|
| `id` | TEXT | 主键,UUID,也是 Milvus 主键 |
| `document_id` | TEXT | 外键,删除文档时级联 |
| `order_index` | INTEGER | 文档内顺序,从 0 开始 |
| `content` | TEXT | 分块原文 |
| `page_number` | INTEGER NULL | PDF 页码,从 1 开始 |
| `char_count` | INTEGER | 字符数 |
| `created_at` | DATETIME | UTC |
`(document_id, order_index)` 唯一,并为 `document_id` 建索引。
### 6.4 `chat_sessions`
| 字段 | 类型 | 约束/用途 |
|---|---|---|
| `id` | TEXT | 主键,UUID |
| `title` | TEXT NULL | 手动标题;未提供时由首次问题前 30 个字符填充 |
| `created_at` | DATETIME | UTC |
| `updated_at` | DATETIME | UTC,按此字段倒序列出 |
### 6.5 `messages`
| 字段 | 类型 | 约束/用途 |
|---|---|---|
| `id` | TEXT | 主键,UUID |
| `session_id` | TEXT | 外键,删除会话时级联 |
| `role` | TEXT | 只允许 `user/assistant` |
| `content` | TEXT | 用户问题或最终回答 |
| `status` | TEXT | `generating/completed/failed` |
| `error_code` | TEXT NULL | assistant 失败原因 |
| `order_index` | INTEGER | 会话内顺序 |
| `created_at` | DATETIME | UTC |
`(session_id, order_index)` 唯一,并为 `(session_id, created_at)` 建索引。用户消息直接为 `completed`;assistant 在运行前创建空内容的 `generating` 占位记录,完成后一次更新内容。
### 6.6 `message_sources`
| 字段 | 类型 | 约束/用途 |
|---|---|---|
| `id` | TEXT | 主键,UUID |
| `message_id` | TEXT | assistant 消息外键,删除消息时级联 |
| `document_id` | TEXT | 文档外键,删除文档时级联 |
| `chunk_id` | TEXT | chunk 外键,删除 chunk 时级联 |
| `citation_label` | TEXT | `S1``S8` |
| `score` | REAL | COSINE 检索分数 |
| `created_at` | DATETIME | UTC |
`(message_id, chunk_id)` 唯一。来源正文和文件名实时关联 `chunks/documents`,不在该表保留正文副本。删除文档后历史回答文本仍保留,但对应来源卡片和来源正文会彻底删除。
## 7. Milvus Lite 设计
数据库文件默认位于 `data/milvus/kbqa.db`collection 名为 `kbqa_chunks`
| 字段 | Milvus 类型 | 用途 |
|---|---|---|
| `chunk_id` | VARCHAR | 主键,使用 SQLite chunk UUID |
| `document_id` | VARCHAR | 按文档过滤和删除 |
| `order_index` | INT64 | 原文顺序 |
| `vector` | FLOAT_VECTOR | `dim=1024` |
向量索引使用 `AUTOINDEX`,距离指标使用 `COSINE`。Milvus 不重复保存 chunk 正文。检索得到 chunk ID 和分数后,应用一次批量查询 SQLite,并丢弃所属文档不是 `indexed` 状态的结果。
索引和重试使用确定性 `chunk_id` 执行 upsert。清理使用 `document_id` filter 删除整篇文档的向量。
## 8. 文档索引流程
1. API 校验文件大小、扩展名、MIME 与内容。
2. 文件以 UUID 文件名写入 `data/raw`,避免路径穿越和同名覆盖。
3. 在一个 SQLite 事务内创建 `documents(status=pending)``index_jobs(status=queued)`
4. 任务提交给进程内 `asyncio.Queue`API 返回 `202`
5. 单消费者将任务和文档改为 `running/indexing`
6. 重试前按 `document_id` 清理旧 Milvus 向量和旧 SQLite chunk。
7. PDF 使用可提取文本加载器;Markdown/TXT 使用文本加载器。无文本 PDF 失败为 `PDF_TEXT_NOT_EXTRACTABLE`
8. 使用 `RecursiveCharacterTextSplitter`,默认 `chunk_size=500``chunk_overlap=50`
9. 在内存中形成 chunk 与确定性 ID,批量调用真实 `text-embedding-v4`
10. SQLite 写入 chunk,文档仍保持 `indexing`
11. Milvus 按 chunk ID upsert 向量。
12. SQLite 将文档改为 `indexed`、记录 `chunk_count`,任务改为 `succeeded`
13. 任一步骤失败时文档和任务改为 `failed`,保存安全错误信息;残留向量在下一次重试或删除时清理。
索引同一时间只处理一篇文档,避免本地数据库竞争和模型并发额度失控。
## 9. 双 Agent 设计
### 9.1 模型工厂
`ChatOpenAI` 使用 DashScope OpenAI 兼容 `base_url`、API key 和 `qwen3.5-flash`。主 Agent 与研究 Agent使用相同模型配置,但分别添加 LangSmith tag`main-agent``research-agent`
Embedding 工厂使用 `OpenAIEmbeddings``text-embedding-v4` 和固定 1024 维输出。应用启动不调用模型;真实连通性由 live 测试验证。
### 9.2 研究 Agent
研究 Agent 使用 `langchain.agents.create_agent` 创建,只注册 `search_knowledge_base` 工具。它接收主 Agent 整理后的独立问题,而不是整个会话历史。
研究 Agent 每轮最多调用检索工具 3 次。每次检索:
- 对查询调用真实 Embedding。
- 从 Milvus 请求候选结果。
- 批量回查 SQLite,只接受 `indexed` 文档的 chunk。
- 返回最多 5 条结果。
- 文本内容供研究 Agent 阅读,完整来源元数据作为 Tool artifact 保存在本次运行消息中。
研究完成后,外层 `research` 工具从 ToolMessage artifact 收集全部结果,按 chunk ID 去重并保留最高分,最多形成 8 条来源,编号为 `S1``S8`
### 9.3 主 Agent
主 Agent 使用 `langchain.agents.create_agent` 创建,只注册 `research` 工具,并读取当前会话最近 20 条用户可见消息。
自定义 LangChain 1.x middleware 强制当前知识问答在生成最终答案前完成一次成功的 `research` 调用,并限制模型/工具调用次数。主 Agent不能访问 Milvus,也不能直接调用底层检索工具。
`research` 工具向主 Agent 返回:
- 研究 Agent 基于工具结果生成的摘要。
- 编号后的原始证据片段。
- 可使用的引用标签集合。
主 Agent提示词要求所有知识性事实基于证据并使用 `[S1]` 形式引用。若研究结果为空,Chat Service 不再让主 Agent自由生成,而是发送固定的资料不足回答。最终答案中不存在的来源标签视为运行错误,不写入成功历史。
严格知识库模式通过“强制研究、空证据短路、引用标签校验”实现。它不能从数学上证明每句话的事实正确性,但会阻断无检索回答和伪造来源。
## 10. 会话与 SSE 流程
1. 校验会话存在且当前没有进行中的生成。
2. 保存 `user/completed` 消息和 `assistant/generating` 占位消息。
3. 读取最近 20 条用户可见历史消息。
4. 调用主 Agent 的 async stream,启用 `messages/custom/updates` 流模式。
5. 研究开始时发送 `status(researching)`
6. `research` 完成后发送去重后的 `sources`
7. 主 Agent开始最终回答时发送 `status(answering)`
8. 只将主 Agent最终回答的 token 映射为 `token` 事件;不暴露研究 Agent草稿或内部工具消息。
9. 累积完整回答,校验引用标签,在一个 SQLite 事务内更新 assistant 消息并写入 `message_sources`
10. 数据持久化完成后发送 `done`
研究期间每 15 秒发送 SSE 注释 `: ping`。前端不展示 heartbeat。
浏览器使用 `AbortController` 取消请求。客户端断开、模型超时或服务异常时取消下游任务并将 assistant 占位消息标记为 `failed`;残缺 token 不保存为成功回答。
## 11. SSE 事件契约
`POST /api/v1/chat/stream``Content-Type``text/event-stream`
```text
event: status
data: {"phase":"researching"}
event: sources
data: {"sources":[{"label":"S1","document_id":"...","chunk_id":"...","filename":"rag.pdf","order_index":2,"page_number":3,"content":"...","score":0.92}]}
event: status
data: {"phase":"answering"}
event: token
data: {"content":"RAG"}
event: done
data: {"message_id":"..."}
event: error
data: {"code":"MODEL_TIMEOUT","message":"模型响应超时,请重试","retryable":true}
```
事件 data 必须是单行 JSON。前端 SSE parser 必须正确处理 TCP chunk 在任意字符或行位置切分的情况。
## 12. HTTP API
所有业务 API 使用 `/api/v1` 前缀。
### 12.1 文档
| 方法 | 路径 | 成功响应 | 说明 |
|---|---|---|---|
| POST | `/api/v1/documents` | `202 Document` | 上传、落盘并创建后台任务 |
| GET | `/api/v1/documents` | `200 Page[Document]` | 分页,支持 status 过滤 |
| GET | `/api/v1/documents/{id}` | `200 Document` | 文档详情 |
| GET | `/api/v1/documents/{id}/chunks` | `200 Page[Chunk]` | 分页分块预览 |
| POST | `/api/v1/documents/{id}/retry` | `202 Document` | 只允许 failed 状态 |
| DELETE | `/api/v1/documents/{id}` | `204` | 清理向量、文件和 SQLite 关联数据 |
删除时先把文档标记为 `deleting`,再按顺序删除 Milvus 向量、原文件和 SQLite 关联数据。只有全部成功才返回 204;失败时保留 `deleting` 记录。应用启动时查询所有 `deleting` 文档并继续清理,向量或文件已经不存在时按删除成功处理,保证恢复流程幂等。
### 12.2 会话与问答
| 方法 | 路径 | 成功响应 | 说明 |
|---|---|---|---|
| POST | `/api/v1/sessions` | `201 Session` | 可提供标题 |
| GET | `/api/v1/sessions` | `200 Page[Session]` | 按 updated_at 倒序 |
| GET | `/api/v1/sessions/{id}/messages` | `200 Page[Message]` | 返回用户可见消息和有效来源 |
| DELETE | `/api/v1/sessions/{id}` | `204` | 级联删除消息和引用 |
| POST | `/api/v1/chat/stream` | `200 text/event-stream` | `{session_id, query}` |
同一会话同时只允许一个生成任务;冲突返回 409。未提供标题的会话使用 NULL 保存,界面显示“新会话”;首次问题将标题更新为问题前 30 个字符,不额外调用模型生成标题。
### 12.3 健康检查
`GET /health` 检查应用、SQLite 与 Milvus Lite,返回各组件状态,但不调用付费 Chat 或 Embedding 服务。
## 13. 文件校验与限制
- 默认最大上传大小为 20 MiB,可通过配置修改。
- 只允许 `.pdf``.md``.markdown``.txt`
- 同时校验扩展名、MIME 与内容签名,不只信任浏览器提供的 MIME。
- 原始文件名只用于展示;落盘文件名使用 document UUID。
- Markdown 和 TXT 必须能按 UTF-8 解码。
- PDF 必须至少提取到一段非空文本,否则异步任务失败并提示不支持扫描件。
- 同内容文件允许重复上传;SHA-256 用于诊断和完整性,不用于拒绝重复。
## 14. 错误处理
普通 JSON API 的错误格式:
```json
{
"error": {
"code": "UNSUPPORTED_FILE_TYPE",
"message": "仅支持 PDF、Markdown 和 TXT 文件",
"request_id": "..."
}
}
```
状态码约定:
- 413:文件过大。
- 415:文件类型不支持。
- 404:文档或会话不存在。
- 409:资源状态冲突或会话正在生成。
- 422:参数校验失败。
- 502:模型上游返回错误。
- 504:模型调用超时。
- 503SQLite 或 Milvus Lite 暂不可用。
SSE 建立后发生的错误使用 `event: error`,不能再尝试改变 HTTP 状态码。服务端日志记录 request ID 和异常堆栈,但不记录密钥、完整提示词或文档正文;前端只显示安全错误摘要和是否可重试。
## 15. 前端交互
### 15.1 桌面三栏
- 左栏固定约 220–260 px:产品标识、对话/文档导航、新建会话和历史会话。
- 中栏自适应:对话消息、研究/回答状态和固定底部输入框。
- 右栏固定约 300–340 px:当前回答的来源列表和来源详情。
来源以 `S1``S2` 展示。点击来源卡片后直接在右栏原位展开完整 chunk,不使用遮挡对话的 modal。用户切换某条历史 assistant 消息时,右栏同步切换到该消息的有效来源。
文档被删除后,历史回答中的引用标签仍可见,但不存在对应 `message_sources` 记录;前端将其渲染为不可点击的“来源已删除”,不显示原文件名或正文。
### 15.2 文档页面
文档页沿用同一工作台框架:
- 左栏显示主导航、状态筛选和计数。
- 中栏显示上传入口、文档列表、类型、分块数和索引状态。
- 右栏显示选中文档信息和分页 chunk 预览。
- failed 文档显示安全失败原因和重试按钮。
### 15.3 响应式
- `>= 1100 px`:完整三栏。
- `7681099 px`:右侧来源变为可打开的抽屉。
- `< 768 px`:左侧导航与来源均为抽屉,聊天区单栏显示。
首版以桌面浏览器为主要验收目标,同时保证窄屏关键操作可用。
### 15.4 路由
- `/chat`:无会话时显示空状态或新建会话入口。
- `/chat/:sessionId`:指定会话的多轮问答。
- `/documents`:文档列表与上传入口。
- `/documents/:documentId`:指定文档及其分块详情。
### 15.5 客户端状态
- 索引页面每 2 秒轮询 pending/indexing 文档,进入 indexed/failed 后停止。
- SSE 临时 token 只保存在当前页面状态;收到 done 后重新读取持久化消息。
- 研究中展示阶段提示,但 heartbeat 不产生视觉提示。
- failed assistant 消息显示可重新发送原问题的操作。
- 没有 indexed 文档时,聊天空状态引导用户先上传文档。
## 16. 配置与密钥
应用配置使用 `KBQA_` 前缀;LangSmith SDK 使用其官方 `LANGSMITH_` 环境变量。`.env.example` 只包含占位符,至少包括:
```dotenv
KBQA_DASHSCOPE_API_KEY=
KBQA_DASHSCOPE_BASE_URL=
KBQA_CHAT_MODEL=qwen3.5-flash
KBQA_EMBEDDING_MODEL=text-embedding-v4
KBQA_EMBEDDING_DIM=1024
KBQA_DATABASE_URL=sqlite+aiosqlite:///./data/kbqa.sqlite3
KBQA_MILVUS_URI=./data/milvus/kbqa.db
KBQA_CHUNK_SIZE=500
KBQA_CHUNK_OVERLAP=50
KBQA_RETRIEVAL_TOP_K=5
KBQA_MAX_RESEARCH_SEARCHES=3
KBQA_MAX_SOURCES=8
KBQA_HISTORY_MESSAGE_LIMIT=20
KBQA_MAX_UPLOAD_MIB=20
KBQA_RUN_LIVE_TESTS=0
KBQA_LIVE_TEST_FILE=
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=
LANGSMITH_PROJECT=kbqa-local
```
按用户当前授权,`1.md` 中现有 DashScope 与 LangSmith 密钥可用于本地真实测试;该文件必须保持 Git 忽略,真实值只映射到同样被忽略的 `.env`。密钥不得出现在日志、测试报告、截图或提交中,项目验收后仍建议在提供商控制台轮换。本地 `test.txt` 作为用户提供的大型真实知识文件,通过 `KBQA_LIVE_TEST_FILE` 指向,不提交到 Git。LangSmith 会接收 Agent 输入、工具结果和运行轨迹,这是产品已确认的外部数据传输。
开发环境 CORS 默认只允许 `http://localhost:5173``http://127.0.0.1:5173`,可通过显式配置增加本地来源,不允许通配符来源。
## 17. 测试与验收
### 17.1 开发方式
本项目不使用 TDD。每个功能先按已批准设计实现,再补充并执行相应验证。测试不得使用 Chat 或 Embedding Mock。
### 17.2 普通验证
普通验证不进入真实模型路径:
- Python 格式、lint 和类型/导入检查。
- SQLite migration、repository、状态机和级联删除验证。
- 文件类型、大小和路径安全验证。
- 文本加载与分块验证。
- SSE 编码和前端增量解析验证。
- TypeScript、ESLint 和生产构建验证。
普通测试使用独立临时 SQLite 和 Milvus Lite 文件,不接触开发数据。
### 17.3 Live API 集成测试
只有显式设置 `KBQA_RUN_LIVE_TESTS=1` 才运行 live 测试。测试启动真实 Uvicorn 进程,通过 localhost HTTP 请求真实 API,并使用真实 DashScope 与 LangSmith 配置。
必须覆盖:
1. 上传真实 PDF、Markdown 和 TXT fixture。
2. 轮询直到后台真实 Embedding 完成,验证文档 indexed 且 chunk_count 大于 0。
3. 创建会话并调用真实 `/chat/stream`
4. 验证 status、sources、token、done 事件顺序和来源关联。
5. 发送依赖上一轮语境的追问,验证历史会话和新引用。
6. 询问 fixture 中不存在的事实,验证资料不足响应。
7. 重启 Uvicorn,验证文档、会话、消息和来源仍存在。
8. 删除文档和会话,验证原文件、SQLite 关联记录和 Milvus 向量均被清理。
模型输出测试只断言语义边界、引用存在性和结构,不断言固定措辞。
### 17.4 Agent-browser 端到端测试
使用 `agent-browser` 操作真实 Vite 页面和真实 FastAPI
- 上传文档并观察 pending/indexing/indexed 状态。
- 新建会话并发送真实问题。
- 验证研究阶段、流式文本增长和右栏来源。
- 点击来源并验证完整 chunk 详情。
- 发送多轮追问并刷新页面验证历史恢复。
- 验证索引失败重试、删除确认、空状态和错误提示。
- 在桌面三栏与窄屏抽屉视口各执行关键流程。
- 保存关键状态截图作为验收证据。
## 18. 参考资料
- [LangChain v1](https://docs.langchain.com/oss/python/releases/langchain-v1)
- [LangChain subagents](https://docs.langchain.com/oss/python/langchain/multi-agent/subagents)
- [LangChain streaming](https://docs.langchain.com/oss/python/langchain/streaming)
- [LangChain middleware](https://docs.langchain.com/oss/python/langchain/middleware/overview)
- [Milvus schema design](https://github.com/milvus-io/milvus-docs/blob/v3.0.x/site/en/aiTools/schema_design.md)
- [Milvus Lite](https://github.com/milvus-io/milvus-docs/blob/v3.0.x/site/en/getstarted/milvus_lite.md)
## 19. 验收判定
只有同时满足以下条件才认为首版完成:
- 已在干净环境中通过 `uv``npm` 安装并启动。
- 所有普通验证通过。
- live API 测试真实调用 Chat 与 Embedding 并通过。
- agent-browser 端到端流程通过并留存关键截图。
- 上传、索引、双 Agent、SSE、引用、多轮历史、重启恢复和完整删除链路全部通过。
- 代码中不存在 PostgreSQL/psycopg、LangChain 0.3.x Agent API、模型 Mock 或明文密钥。