# 文档问答智能体设计方案 > **目标**: 构建一个可以不断加入文件、然后针对内容提问的问答智能体。 > **架构**: 三层复用架构(Skill → Pattern → Orchestration)+ Claude Code 规划 + 本地 LLM 执行。 > **成本**: Claude Code 仅在首次设置时介入($0.03),日常问答完全由本地模型执行($0)。 --- ## 一、核心设计 ### 两个阶段 问答智能体有两个截然不同的工作阶段,各自对应独立的处理流水线: ``` 阶段 1: 喂文件 (Ingestion) 用户: "加入这3个PDF和5个代码文件" 系统: 读取 → 分块 → 索引 → 存入知识库 特点: 批量、可并行、不需要强推理 阶段 2: 提问 (Query) 用户: "这些文件里提到了哪些安全风险?" 系统: 检索相关块 → 生成回答 → 标注来源 → 质量审查 特点: 交互式、需要推理、需要事实验证 ``` 两个阶段共享同一个知识库(向量数据库),但使用不同的 Pattern 组合。 ### 架构全景 ``` ┌─────────────────────────────────────────────────────────────┐ │ Claude Code (规划层) │ │ │ │ 仅在以下场景介入: │ │ 1. 首次设置: 分析文件类型 → 选择分块策略 → 生成配置 │ │ 2. 异常处理: fact-checker 连续失败 → 调整策略 │ │ 3. 新需求: 用户要求新的问答能力 → 扩展配置 │ │ │ │ 日常问答不介入 — 全部由 PaaS + 本地模型处理 │ └────────────────────────────┬────────────────────────────────┘ │ MCP / REST API ▼ ┌─────────────────────────────────────────────────────────────┐ │ lambdagentpaas (执行层) │ │ │ │ Layer 1: Skills │ │ file-reader, chunk-splitter, knowledge-indexer, │ │ context-retriever, answer-generator, source-citer, │ │ fact-checker │ │ │ │ Layer 2: Patterns │ │ 喂文件: map_reduce (分而治之模式) │ │ 提问: pipeline + review (流水线 + 审查模式) │ │ │ │ Layer 3: Orchestration │ │ qa-agent.yml (声明式 YAML 编排) │ │ │ │ 执行引擎: CEK Machine (成本监控 + 循环检测) │ │ 知识库: ChromaDB (向量存储 + 语义检索) │ │ 本地 LLM: Ollama (qwen2.5:32b) / DashScope (qwen3-max) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 二、Layer 1: Skill 清单 ### 7 个 Skill 及其角色 | Skill | 功能 | 输入类型 | 输出类型 | 是否需要 LLM | 复用来源 | |-------|------|---------|---------|-------------|---------| | `file-reader` | 读取文件内容(支持 txt/md/py/pdf) | 文件路径 | 文本内容 | 否(纯工具) | 已有: ReadFile | | `chunk-splitter` | 将长文本切分为适合检索的块 | 长文本 | 文本块列表 | 否(纯工具) | 已有: ChunkSplit | | `knowledge-indexer` | 将文本块写入向量知识库 | 文本块 | 索引确认 | 否(向量化) | 已有: KBAdd | | `context-retriever` | 根据问题检索相关文本块 | 问题字符串 | 相关块列表 | 否(向量搜索) | 已有: KBSearch | | `answer-generator` | 基于检索上下文生成回答 | 问题+上下文 | 回答文本 | **是** | 新建 | | `source-citer` | 为回答标注来源文件和位置 | 回答+来源块 | 带引用的回答 | **是** | 新建 | | `fact-checker` | 验证回答是否有原文支撑 | 回答+原文 | VERIFIED/修改意见 | **是** | 新建 | **关键发现**: 7 个 Skill 中只有 3 个需要 LLM 调用,其余 4 个是纯工具。喂文件阶段完全不需要 LLM。 ### Skill 定义 #### file-reader ```yaml skillId: file-reader name: 文件读取器 type: simple description: "读取各种格式的文件,提取纯文本内容" tools: [ReadFile] outputSchema: type: object properties: content: { type: string, description: "文件文本内容" } file_path: { type: string } file_type: { type: string, description: "txt|md|py|pdf|json" } char_count: { type: integer } tags: [ingestion, file, reader] ``` 实现逻辑: ```python def file_reader(input_val): """读取文件,支持多种格式。""" path = str(input_val).strip() if path.endswith(".pdf"): # PDF: 使用 PyPDF2 import PyPDF2 with open(path, "rb") as f: reader = PyPDF2.PdfReader(f) text = "\n".join(page.extract_text() or "" for page in reader.pages) else: # 文本文件: 直接读取 with open(path, "r", encoding="utf-8") as f: text = f.read() return json.dumps({ "content": text, "file_path": path, "file_type": path.rsplit(".", 1)[-1] if "." in path else "txt", "char_count": len(text), }, ensure_ascii=False) ``` #### chunk-splitter ```yaml skillId: chunk-splitter name: 文本分块器 type: simple description: "将长文本按语义边界切分为适合检索的块(默认 512 token)" outputSchema: type: object properties: chunks: { type: array, items: { type: string } } total_chunks: { type: integer } strategy: { type: string, description: "paragraph|token|code_function" } tags: [ingestion, chunking, text-processing] ``` 分块策略: ``` Markdown/文本文件: 1. 按段落分割(\n\n) 2. 如果段落 > 512 token → 按句子再分 3. 如果段落 < 100 token → 与下一段合并 4. 每个块附带元数据: {source: "file.md", section: "第3章", offset: 2048} 代码文件: 1. 按函数/类分割(识别 def/class 关键字) 2. 保留完整函数体(不在函数中间切断) 3. 每个块附带: {source: "api.py", function: "get_user", line: 42} PDF 文件: 1. 按页分割 2. 页内按段落再分 3. 每个块附带: {source: "paper.pdf", page: 5} ``` #### knowledge-indexer ```yaml skillId: knowledge-indexer name: 知识库索引器 type: simple description: "将文本块写入 ChromaDB 向量知识库" inputSchema: type: object properties: chunks: { type: array, items: { type: string } } metadata: { type: object, description: "文件来源等元数据" } collection: { type: string, default: "default" } outputSchema: type: object properties: indexed_count: { type: integer } collection: { type: string } tags: [ingestion, indexing, vector-db] ``` #### context-retriever ```yaml skillId: context-retriever name: 上下文检索器 type: simple description: "根据问题从知识库检索最相关的文本块(top-k 语义搜索)" inputSchema: type: object properties: query: { type: string } top_k: { type: integer, default: 5 } collection: { type: string, default: "default" } outputSchema: type: object properties: results: type: array items: type: object properties: content: { type: string } source: { type: string } relevance: { type: number } total_found: { type: integer } tags: [query, retrieval, vector-search] ``` #### answer-generator (需要 LLM) ```yaml skillId: answer-generator name: 回答生成器 type: simple description: "基于检索到的上下文,用 LLM 生成准确回答" model: provider: ollama # 本地模型 name: qwen2.5:32b temperature: 0.3 systemPrompt: | 你是一个严谨的问答助手。根据提供的参考文档回答用户问题。 规则: 1. 只基于提供的文档内容回答,不要编造信息 2. 如果文档中没有相关信息,明确说明"文档中未找到相关信息" 3. 回答要具体,引用文档中的原文作为依据 4. 对于数字、日期等事实性信息,必须与文档原文一致 参考文档: {context} 请回答以下问题: outputSchema: type: object properties: answer: { type: string } confidence: { type: string, enum: [high, medium, low] } key_points: { type: array, items: { type: string } } tags: [query, answer, llm, rag] ``` #### source-citer (需要 LLM) ```yaml skillId: source-citer name: 来源标注器 type: simple description: "为回答中的每个论点标注来源文件和位置" model: provider: ollama name: qwen2.5:32b temperature: 0.0 systemPrompt: | 你是一个引用标注专家。给定一个回答和原始文档块, 为回答中的每个关键论点添加来源引用。 格式: 在每个论点后添加 [来源: 文件名 位置] 例如: "系统使用 Redis 作为缓存 [来源: architecture.md §4.2]" 回答: {answer} 原始文档块: {sources} tags: [query, citation, llm] ``` #### fact-checker (需要 LLM) ```yaml skillId: fact-checker name: 事实核查器 type: simple description: "验证回答中的每个论点是否有原文支撑。审查模式的审查者。" model: provider: ollama name: qwen2.5:32b temperature: 0.0 systemPrompt: | 你是一个严格的事实核查员。检查回答中的每个论点是否有原文支撑。 检查标准: 1. 每个事实性陈述必须在原文中有对应内容 2. 数字、日期必须与原文完全一致 3. 不能有原文未提及的推测性内容 如果所有论点都有据可查,回复: VERIFIED 如果发现问题,回复: REJECTED: [具体指出哪个论点缺乏依据] 回答: {answer} 原文: {original_text} tags: [query, fact-check, review, llm] ``` --- ## 三、Layer 2: Pattern 组合 ### 喂文件阶段: 分而治之模式 (map_reduce) ``` 输入: 文件路径列表 │ ▼ ┌─ file-lister ─┐ 拆分: 列出所有文件 │ │ ▼ ▼ ▼ ▼ 并行映射: 每个文件独立处理 ┌───┐┌───┐┌───┐┌───┐ │ F1 ││ F2 ││ F3 ││ F4 │ file-reader >> chunk-splitter >> knowledge-indexer └─┬─┘└─┬─┘└─┬─┘└─┬─┘ │ │ │ │ ▼ ▼ ▼ ▼ 合并: 汇总索引统计 └────┴────┴────┘ │ ▼ index-summarizer "已索引 4 个文件,共 87 个块" ``` λA 形式表征: ``` ingestion = map_reduce( splitter = file_lister, mapper = file_reader >> chunk_splitter >> knowledge_indexer, reducer = index_summarizer ) 类型: List(FilePath) →^{io} IndexSummary 效果: io (文件读取 + 向量写入) 成本: 纯工具,无 LLM 调用,$0 ``` ### 提问阶段: 流水线 + 审查模式 (pipeline + review) ``` 问题: "有哪些性能优化建议?" │ ▼ context-retriever 检索: 从知识库找 top-5 相关块 │ ▼ answer-generator (LLM) 生成: 基于上下文回答 (本地 Qwen) │ ▼ source-citer (LLM) 引用: 标注来源文件和位置 │ ▼ fact-checker (LLM) 审查: 每个论点有原文支撑? │ ┌────┴────┐ │ │ VERIFIED REJECTED │ │ ▼ ▼ 输出 回到 answer-generator (review 模式, 最多 2 轮) ``` λA 形式表征: ``` query = review( producer = pipeline( context_retriever, answer_generator, source_citer ), reviewer = fact_checker, max_rounds = 2, approval_keyword = "VERIFIED" ) 类型: Question →^{io · llm³ · (llm)²} VerifiedAnswer 效果: io (检索) + llm × 3 (生成+引用+审查) × 最多 2 轮 成本: 最好情况 (1 轮通过): 3 次本地 LLM = $0 最坏情况 (2 轮): 6 次本地 LLM = $0 (本地模型无 API 成本) ``` --- ## 四、Layer 3: 完整 YAML 编排 ```yaml # qa-agent.yml — 文档问答智能体 agentId: qa-agent-v1 name: 文档问答智能体 description: > 支持不断加入文件,然后针对内容进行提问。 喂文件阶段使用分而治之模式并行处理。 提问阶段使用 RAG 检索 + LLM 回答 + 事实核查。 type: react model: provider: ollama name: qwen2.5:32b temperature: 0.3 maxTokens: 4096 conversation: true maxHistoryTokens: 40000 systemPrompt: | 你是一个文档问答助手。你有两个能力: 1. **加入文件**: 当用户提供文件路径时,调用 ingest_files 工具索引文件 2. **回答问题**: 当用户提问时,调用 query_knowledge 工具从已索引文档中检索并回答 始终基于文档内容回答,不要编造信息。如果文档中没有相关信息,坦诚告知。 react: maxSteps: 15 toolTimeout: 60 mcp: localTools: - ingest_files # 喂文件工具 (map_reduce 模式) - query_knowledge # 提问工具 (pipeline + review 模式) - list_knowledge # 查看已索引文件 - terminate # 结束对话 memory: enabled: true strategy: local size: 50 runtime: engine: cek costBudget: 1.00 maxSteps: 10000 rag: enabled: true provider: chromadb collection: qa_default chunkSize: 512 chunkOverlap: 50 topK: 5 ``` --- ## 五、Claude Code 的角色 ### 何时介入 ``` 场景 1: 首次设置 (介入) 用户: "我要对这些文件做问答" Claude Code: 1. 分析文件类型 (3 PDF + 5 Python + 4 Markdown) 2. 决定分块策略: - PDF → 按页+段落分块 - Python → 按函数分块 - Markdown → 按标题层级分块 3. 生成 qa-agent.yml 配置 4. 调用 PaaS API 部署 5. 触发首次喂文件 场景 2: 日常问答 (不介入) 用户: "API 文档里有哪些鉴权方式?" PaaS 全本地执行: context-retriever → answer-generator → source-citer → fact-checker → 全程 $0 场景 3: 追加文件 (不介入) 用户: "再加入这个新文件 design-v2.md" PaaS 全本地执行: file-reader → chunk-splitter → knowledge-indexer → 增量索引,不重建 场景 4: 质量异常 (介入) fact-checker 连续 2 轮 REJECTED PaaS 上报 Claude Code: "回答 '数据库使用 MySQL' 被拒绝,原文显示使用 PostgreSQL" Claude Code 分析: - 检索结果不相关 → 调大 topK 或换检索策略 - 本地模型理解力不够 → 换更强模型或改 prompt - 知识库过时 → 提示用户重新索引 ``` ### MCP 工具调用流程 ``` Claude Code 配置: .claude/settings.json → "lambdagent": { "command": "python3 -m lambdagent.mcp_server" } Claude Code 调用 (首次设置): 1. lint_agent_config(qa-agent.yml) → 检查配置无错误 2. estimate_agent_cost(qa-agent.yml) → 预估成本 $0(本地模型) 3. deploy_and_run(qa-agent.yml) → 部署到 PaaS 用户后续直接与 PaaS 交互: POST /api/v1/agents/qa-agent-v1/run {"input": "这些文件里有哪些安全风险?"} ``` --- ## 六、一次完整问答流程 ### 喂文件 ``` 用户: "加入 /docs 目录下的所有文件" PaaS 执行 (map_reduce 模式): Step 1: file-lister 扫描 /docs → 发现 12 个文件 [paper.pdf, api.py, auth.py, config.md, deploy.md, ...] Step 2: 并行 12 路 (AsyncPar, 本地执行) ┌─ file-reader("paper.pdf") → 提取 PDF 文本 (22KB) │ chunk-splitter(text, strategy="paragraph") → 28 个块 │ knowledge-indexer(chunks, source="paper.pdf") → ChromaDB 写入 ✓ │ ├─ file-reader("api.py") → 读取 Python (8KB) │ chunk-splitter(text, strategy="code_function") → 15 个块 │ knowledge-indexer(chunks, source="api.py") → ChromaDB 写入 ✓ │ ├─ file-reader("config.md") → 读取 Markdown (3KB) │ chunk-splitter(text, strategy="heading") → 8 个块 │ knowledge-indexer(chunks, source="config.md") → ChromaDB 写入 ✓ │ └─ ... (其余 9 个文件并行处理) Step 3: index-summarizer "已索引 12 个文件,共 184 个文本块。 按类型: 3 PDF (76 块), 5 Python (52 块), 4 Markdown (56 块)。 知识库: qa_default (ChromaDB)" 总耗时: ~5 秒 (并行, 无 LLM 调用) 总成本: $0 ``` ### 提问 ``` 用户: "这些文件里提到了哪些性能优化建议?" PaaS 执行 (pipeline + review 模式): Step 1: context-retriever 查询: "性能优化建议" ChromaDB 语义搜索 → top-5 相关块: [0.92] perf.py L42-58: "def optimize_query(): 使用连接池..." [0.87] architecture.md §4.2: "缓存策略: Redis 作为..." [0.85] deploy.pdf P5: "CDN 配置: 静态资源..." [0.79] api.py L120: "def cached_response(): @lru_cache..." [0.71] config.md §3: "数据库配置: pool_size=20..." 耗时: 50ms, 成本: $0 Step 2: answer-generator (本地 Qwen-32B) 输入: "基于以下参考文档回答问题... [块1] perf.py L42-58: def optimize_query()... [块2] architecture.md §4.2: 缓存策略... ... 问题: 这些文件里提到了哪些性能优化建议?" 输出: "根据文档,有以下性能优化建议: 1. 使用数据库连接池 (pool_size=20) 减少连接开销 2. 对热点查询使用 Redis 缓存 3. API 响应使用 lru_cache 装饰器 4. 静态资源通过 CDN 分发 5. 数据库查询使用索引优化" 耗时: 3s, 成本: $0 Step 3: source-citer (本地 Qwen-32B) 输入: 回答 + 5 个原始块 输出: "1. 使用数据库连接池 (pool_size=20) [来源: config.md §3, perf.py L42] 2. 对热点查询使用 Redis 缓存 [来源: architecture.md §4.2] 3. API 响应使用 lru_cache [来源: api.py L120] 4. 静态资源通过 CDN 分发 [来源: deploy.pdf 第5页] 5. 数据库查询使用索引优化 [来源: perf.py L55]" 耗时: 2s, 成本: $0 Step 4: fact-checker (本地 Qwen-32B) 逐条验证: ✓ 论点1: config.md 原文 "pool_size: 20" 匹配 ✓ 论点2: architecture.md 原文 "Redis 作为缓存层" 匹配 ✓ 论点3: api.py 原文 "@lru_cache(maxsize=256)" 匹配 ✓ 论点4: deploy.pdf 原文 "CDN 配置" 匹配 ✓ 论点5: perf.py 原文 "CREATE INDEX" 匹配 输出: "VERIFIED" 耗时: 2s, 成本: $0 审查通过 (review 模式第 1 轮即通过,无需重试) 最终输出: "根据文档,有以下性能优化建议: 1. 使用数据库连接池 (pool_size=20) 减少连接开销 [来源: config.md §3, perf.py L42] 2. 对热点查询使用 Redis 缓存 [来源: architecture.md §4.2] 3. API 响应使用 lru_cache 装饰器 [来源: api.py L120] 4. 静态资源通过 CDN 分发 [来源: deploy.pdf 第5页] 5. 数据库查询使用索引优化 [来源: perf.py L55] ✓ 以上所有建议均已通过原文验证。" 总耗时: ~7 秒 总成本: $0 (全部本地 LLM) ``` --- ## 七、成本对比 | 方案 | 喂文件 (12 文件) | 单次提问 | 100 次提问 | 总成本 | |------|-----------------|---------|-----------|--------| | **纯 Claude Code** | 12 次 LLM = $0.36 | $0.03 | $3.00 | **$3.36** | | **纯 ChatGPT + RAG** | 向量化 $0.02 | $0.01 | $1.00 | **$1.02** | | **lambdagentpaas (本地)** | $0 | $0 | $0 | **$0** | | **混合模式 (推荐)** | 规划 $0.03 + 执行 $0 | $0 | $0 | **$0.03** | 混合模式: Claude Code 只在首次设置花 $0.03,之后所有操作完全免费。 --- ## 八、增量更新 ### 追加文件 ``` 用户: "再加入 design-v2.md" PaaS 执行: 1. file-reader("design-v2.md") → 读取内容 2. 检查知识库: "design-v2.md" 是否已索引? ├── 否 → chunk-splitter → knowledge-indexer (新增) └── 是 → 内容有变化? ├── 否 → 跳过 ("已是最新") └── 是 → 删除旧块 → 重新分块 → 重新索引 (更新) 3. 汇报: "已更新 design-v2.md: 新增 12 个块" ``` ### 删除文件 ``` 用户: "移除 old-spec.md 的内容" PaaS 执行: 1. 从 ChromaDB 删除所有 source="old-spec.md" 的块 2. 汇报: "已移除 old-spec.md: 删除 8 个块,知识库剩余 176 个块" ``` ### 查看索引状态 ``` 用户: "现在知识库里有什么?" PaaS 执行 (list_knowledge 工具): "当前知识库 qa_default 状态: - 总文件: 12 个 - 总块数: 184 个 - 文件清单: paper.pdf (28 块, 2024-03-15 索引) api.py (15 块, 2024-03-15 索引) config.md (8 块, 2024-03-15 索引) design-v2.md (12 块, 2024-03-16 更新) ..." ``` --- ## 九、质量保证机制 ### 三道防线 ``` 第 1 道: 检索质量 context-retriever 返回 relevance score 如果 top-1 score < 0.5 → 告诉用户 "文档中可能没有直接相关的内容" 第 2 道: 生成质量 answer-generator 的 systemPrompt 要求: "只基于提供的文档内容回答,不要编造信息" 第 3 道: 事实核查 (review 模式) fact-checker 逐条验证,不通过则重新生成 最多 2 轮重试,2 轮后仍不通过: → 输出部分结果 + 标注 "以下论点未通过验证: ..." → 如果 Claude Code 在线,上报异常请求协助 ``` ### 常见失败模式及对策 | 失败模式 | 原因 | 对策 | |---------|------|------| | 检索到不相关内容 | 问题表述与文档用词差异大 | 增大 topK, 加入关键词检索 | | 回答编造信息 | 本地模型能力不足 | 换更强模型, 或缩短上下文专注相关块 | | 来源标注错误 | 块元数据丢失 | 检查 chunk-splitter 的元数据保留 | | fact-checker 过于严格 | prompt 要求太严 | 调整 "宽松模式": 允许合理推断 | | 跨文件推理失败 | 每次只检索 5 个块,可能分散 | 增大 topK 或分两次检索合并 | --- ## 十、实现步骤 ``` Step 1: 注册 Skill (2 天) 复用: file-reader (ReadFile), chunk-splitter (ChunkSplit), knowledge-indexer (KBAdd), context-retriever (KBSearch) 新建: answer-generator, source-citer, fact-checker → 写 3 个 Skill YAML + 注册到 SkillRegistry Step 2: 组合 Pattern (1 天) 喂文件: map_reduce(file-lister, pipeline(reader>>splitter>>indexer), summarizer) 提问: review(pipeline(retriever>>answerer>>citer), fact-checker) → 调用 patterns.py 已有的 map_reduce_pattern 和 review_pattern Step 3: 写编排配置 (0.5 天) qa-agent.yml (如上方第四节) → from_config 编译 + lint 验证 Step 4: 实现工具包装 (1 天) ingest_files 工具: 封装 map_reduce 喂文件流水线 query_knowledge 工具: 封装 pipeline + review 提问流水线 list_knowledge 工具: 查看索引状态 → 注册为 ReAct 的 localTools Step 5: 集成测试 (1 天) 测试 1: 喂入 5 个文件 → 验证索引数量正确 测试 2: 提问 → 验证回答引用了正确来源 测试 3: 提问无关内容 → 验证回答 "文档中未找到" 测试 4: 追加/删除文件 → 验证增量索引正确 测试 5: fact-checker 触发重试 → 验证 review 模式工作 Step 6: 部署 (0.5 天) lambdagent run qa-agent.yml --engine cek 或 PaaS API: POST /api/v1/agents (部署到平台) 总计: ~6 天 ``` --- ## 十一、扩展方向 ### 多知识库 ```yaml # 支持多个独立知识库 rag: collections: code: { description: "源代码", chunkStrategy: "code_function" } docs: { description: "文档", chunkStrategy: "heading" } papers: { description: "论文", chunkStrategy: "paragraph" } # 提问时指定搜索范围 query_knowledge --collection code "认证模块的实现逻辑" query_knowledge --collection all "系统整体架构" ``` ### 对话式追问 ``` 用户: "有哪些安全风险?" Agent: "根据文档,有 3 个安全风险: ..." 用户: "第 2 个能详细说说吗?" Agent: (ConversationLam 保留对话历史) "关于 SQL 注入风险,文档中提到..." [来源: security.md §2.3] ``` ### 多模态 ``` 未来扩展: - 图片文件: OCR 提取文字 → 索引 - 表格文件: CSV/Excel → 结构化解析 → 索引 - 音频文件: 语音转文字 → 索引 ```