552 lines
26 KiB
Markdown
552 lines
26 KiB
Markdown
# 双 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 + ReadableStream:SSE 流式问答
|
||
│
|
||
▼
|
||
单进程 FastAPI 应用
|
||
├─ API 路由与统一错误处理
|
||
├─ 文档、会话和问答应用服务
|
||
├─ SQLite 持久任务 + asyncio 单消费者索引 Worker
|
||
└─ LangChain 1.x 双 Agent
|
||
├─ 主 Agent:research 工具
|
||
└─ 研究 Agent:search_knowledge_base 工具
|
||
│
|
||
┌──────────┼──────────┐
|
||
▼ ▼ ▼
|
||
SQLite Milvus Lite data/raw
|
||
│
|
||
├─ DashScope:真实 Chat 与 Embedding
|
||
└─ LangSmith:Agent 与工具追踪
|
||
```
|
||
|
||
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 或 Agent;Agent 工具不直接管理 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:模型调用超时。
|
||
- 503:SQLite 或 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`:完整三栏。
|
||
- `768–1099 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 或明文密钥。
|