qa-agent-design.md 25 KB

文档问答智能体设计方案

目标: 构建一个可以不断加入文件、然后针对内容提问的问答智能体。 架构: 三层复用架构(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

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]

实现逻辑:

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

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

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

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)

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)

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)

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 编排

# 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 天

十一、扩展方向

多知识库

# 支持多个独立知识库
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 → 结构化解析 → 索引
  - 音频文件: 语音转文字 → 索引