目标: 构建一个可以不断加入文件、然后针对内容提问的问答智能体。 架构: 三层复用架构(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) │
└─────────────────────────────────────────────────────────────┘
| 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。
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)
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}
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]
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]
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]
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]
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]
输入: 文件路径列表
│
▼
┌─ 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
问题: "有哪些性能优化建议?"
│
▼
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 成本)
# 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
场景 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
- 知识库过时 → 提示用户重新索引
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 → 结构化解析 → 索引
- 音频文件: 语音转文字 → 索引