1. 系统定位
面向律师的法律类案检索增强生成系统:输入案情描述 → 五阶段 Agentic 工作流(规划→检索→验证→撰写→引用校验)→ 输出带逐字引用、可溯源的《类案检索分析报告》。核心能力闭环:
| 能力 | 实现要点 |
|---|---|
| Agentic 分析 | 自研星形状态机(非 LangGraph),双回路自纠错(补检 ≤2 轮 / 重生成 ≤2 轮) |
| 停止/恢复 | 每阶段边界保存全量状态检查点(SQLite),pause/cancel/resume(含 failed 恢复) |
| 结果可审计 | GET /tasks/{id}/workflow 聚合五阶段产物、检查点链、事件流、思考摘要、token |
| 引用可信 | 引用登记表 + 三道确定性算法校验(编号/逐字一致/无闲置),未过不交付 |
| Token 指标 | 每次 LLM 调用 XADD wf:llm_tokens(9 字段),Redis 掉线落 SQLite 补发不丢 |
| 实时采集 | web-search-pro 联网搜索 → GLM 结构化 → 与种子库同一管道入库;24h 连续调度 |
| 多端接入 | REST(全部强制 user_id)+ Chat 意图路由 + MCP(HTTP/stdio,12 工具) |
2. 总体架构图
┌──────────────────────────────────────────────────────────────────────────┐
│ 接 入 层(多端) │
│ ┌────────────────────┐ ┌──────────────────┐ ┌────────────────────┐ │
│ │ Vue3 前端(浏览器) │ │ demo/run_demo.py │ │ 外部智能体 │ │
│ │ 左:Chat 中:控制 │ │ 端到端演示脚本 │ │ Claude/Cursor/通义 │ │
│ │ 右:工作台页签 │ └────────┬─────────┘ └─────────┬──────────┘ │
│ └──────────┬─────────┘ │ REST │ MCP │
│ │ REST /api/v1(全部携带 user_id) │ HTTP/stdio │
│ │ + 2.5s 轮询 events/workflow │ │
└──────────────┼──────────────────────┼───────────────────────┼────────────┘
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ FastAPI 单进程服务(backend/app) │
│ │
│ ┌──────────────┐ ┌────────────────┐ ┌─────────────────────────────┐ │
│ │ routes.py │ │ chat.py │ │ mcp_server.py(12 工具) │ │
│ │ REST 全接口 │ │ 意图路由+RAG问答│ │ 工具统一经 HTTP 代理调 REST │ │
│ └──────┬───────┘ └───────┬────────┘ └──────────────┬──────────────┘ │
│ └────────────┬─────┴──────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ WorkflowEngine(engine.py · 星形状态机编排 · asyncio.Task) │ │
│ │ Planner → Researcher → Verifier ⇄补检 → Writer ⇄重生成 → Done │ │
│ │ (planner/researcher/verifier/writer.py,检查点+事件+控制标记) │ │
│ └──────────┬───────────────────────────────────────┬───────────────┘ │
│ │ 四路召回+RRF+争点重排 │ 每次调用 usage │
│ ▼ ▼ │
│ ┌───────────────────────┐ ┌───────────────────────────┐ │
│ │ CaseStore 混合索引 │ │ GLMClient(httpx 异步) │ │
│ │ BM25│向量│法条锚点│图谱 │◄──同一入库──│ chat/chat_json/embed │ │
│ └───────────┬───────────┘ 管道 │ 重试3次+thinking参数容错 │ │
│ │ │ embedding 哈希兜底 │ │
│ ┌───────────▼───────────┐ └─────────────┬─────────────┘ │
│ │ Collector 采集调度器 │ ▼ │
│ │ internet.py │ ┌───────────────────────────┐ │
│ │ 24h 连续/间隔两模式 │ │ TokenMetrics │ │
│ └───────────────────────┘ │ XADD + SQLite 账本 + 补发 │ │
│ └───────────────────────────┘ │
└──────────────────────────┬──────────────────────────────┬────────────────┘
▼ ▼
┌──────────────────────────────────┐ ┌─────────────────────────────────┐
│ 本地持久化(data/) │ │ 外部服务 │
│ · lawsight.db 6 张表 │ │ · 智谱 GLM 开放平台 │
│ · case_store.json 案例主数据 │ │ glm-5.3-flashx(思考型) │
│ · case_vectors.npz 向量矩阵 │ │ embedding-3 / web-search-pro │
└──────────────────────────────────┘ │ · Redis 7:wf:llm_tokens Stream │
└─────────────────────────────────┘
分层职责:接入层只做协议适配 → API 层做 user_id 鉴权/归属校验(400/403)→ 编排层(Engine)独占任务生命周期与检查点纪律 → 领域层(检索/存储/采集/LLM)无状态可复用 → 持久化层 SQLite+JSON+NPZ 三件套。MCP 工具不直连引擎,而是回环代理 REST,保证单实例语义一致。
3. 核心机制设计
3.1 Agentic 工作流状态机(主流程图)
POST /tasks {user_id, fact_description}
│
▼
╔═══════════════════════════════════════════════════════════════╗
║ 主循环 while(true):取 state.next_stage → 执行 → 阶段边界 ║
║ ┌─────────────────────────────────────────────────────────┐ ║
║ │ 边界纪律:seq+1 → 保存全量状态检查点 → 读 kv:ctl:{task_id} │ ║
║ │ · 阶段抛异常 → 检查点(failed) + status=failed + 退出 │ ║
║ │ · ctl=cancel → 检查点(stopped) + status=stopped + 退出 │ ║
║ │ · ctl=pause → 检查点(paused) + status=paused + 退出 │ ║
║ │ · 否则 → 检查点(running) + 事件 stage_completed │ ║
║ └─────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════╝
│
┌──────────┐ ▼ ┌───────────┐
│ plan │ issues/子查询/法条候选 │ research │ ranked(按争点top5)
│ Planner │─────────────────────────► │Researcher │──────────┐
│ chat_json│ 要素抽取·争点分解·HyDE │ 四路召回+ │ │
└──────────┘ (无效输出→单争点降级) │ LLM 重排 │ ▼
▲ └───────────┘ ┌───────────┐
│ │ verify │
│ ┌──────────────────────────────┐ 证据不足 │ Verifier │
└──────┤ revise_plan(gaps) ①补检回路 │◄─round<2───────│ 组装证据包 │
│ 子查询拼接gaps·放宽案由过滤 │ 耗尽→徽章 │ 充分性判定 │
└──────────────────────────────┘ 「证据不足」 └─────┬─────┘
│ 达标
┌──────────┐ 报告+regen_hints ▼
│ write │◄──────────────┐ ┌───────────┐
│ Writer │ 受约束生成 │ │ check │
│ 协议:「…」│[n] 逐字摘录 │ │ 三道校验 │
└────┬─────┘ │ │(确定性算法)│
└──────────────────────►└─────┬─────┘
②重生成回路 regen<2 │ 通过
(hints=问题清单前10条) ▼
┌───────────┐
│ done │
│ 质量徽章: │
│ 完整/部分 │
│ 降级/证据 │
│ 不足 │
└───────────┘
要点:next_stage 是状态机唯一游标(随检查点持久化);两条自纠错回路都有轮次上限(max_research_rounds=2 / max_regen_rounds=2),耗尽后降级交付并打徽章(绝不静默降质)。check 阶段复用 Verifier 的 verify_citations。
3.2 检查点与暂停/恢复(时序图)
用户 API/Engine SQLite Vue3 前端
│ POST /tasks │ │ │
│────────────►│ create_task+事件 │ │
│ │ engine.start ─► asyncio.Task(后台跑状态机) │
│ │ │ │
│ POST /pause │ │ │
│────────────►│ kv_set ctl:{id}=pause │ ← 只设标记,不杀协程 │
│◄────────────┤ 202"阶段边界生效" │ │
│ │ …当前阶段继续执行… │ │
│ │ 阶段边界 ─────────────►│ save_checkpoint(paused) │
│ │ │ status=paused │
│ │ │ event: paused_at_ckpt ──►│ ⏸ 对话流播报
│ │ (服务重启:孤儿 running → paused,同样可恢复) │
│ │ │ │
│ POST /resume│ │ │
│────────────►│ latest_checkpoint ───►│ state=全量JSON快照 │
│ │ state._resumed_from=ck.stage │
│ │ engine.start(task_id, state) 从 next_stage 续跑 │
│ │ │ event: resumed_from ────►│ ▶ 恢复播报
设计取舍:pause 请求不打断进行中的阶段(LLM 调用不浪费、状态不撕裂),在边界原子生效;恢复 = 读最近检查点全量 state 重建执行现场。支持 paused/stopped/failed 三态恢复。
3.3 四路混合检索与融合重排(检索设计图)
Planner 检索计划(retrieval_plan)
subqueries(legal_rewrite法言法语 + hyde_snippet假设文书) + statutes + filters
│
┌────────────┬───────────┴──────────┬─────────────────────┐
▼ ▼ ▼ ▼
┌──────────┐ ┌───────────┐ ┌────────────────┐ ┌────────────────┐
│ 路1 BM25 │ │ 路2 向量 │ │ 路3 法条锚点反查 │ │ 路4 案例图谱扩展│
│ 中文bigram│ │ embedding-3│ │ 引用同一法条的 │ │ 种子案例同案由 │
│ 纯Python │ │ numpy余弦 │ │ chunk 集合 │ │ +共同法条邻接 │
│ 倒排 │ │ (≤3条语义路)│ │ (精确+模糊容错) │ │ (权重1:2) │
└─────┬────┘ └─────┬─────┘ └───────┬────────┘ └───────▲────────┘
└────────────┴──────┬─────────────┴─────────────────────┘
▼
RRF 融合#1 score=Σ 1/(60+rank)
│ top10 案例 = 图谱扩展种子 ──────────┘
▼
RRF 融合#2(四路全量,保留60)
▼
结构化后过滤(case_type / cause)
▼
争点级重排:LLM 对 争点×30候选块 打 0-100 分
(LLM 失败/漏评 → 词面重合度兜底,重排不中断)
▼
ranked = {issue_id: [top5 × {chunk_id, score, reason}]}
RRF 两段式融合是关键设计:图谱扩展依赖前三轮的融合结果选种子,因此拆成"三路融合→选种→四路融合"两步。
3.4 证据包与引用可信链路(防幻觉设计图)
ranked(争点×证据块) Writer 提示词(受约束生成协议)
│ ▲
▼ │ 硬性协议:
┌────────────────────┐ quote = 块文本前200字 │ · 事实陈述必须带 [n]
│ 证据包 + 引用登记表 │ ────────────────────────► │ · 引文「…」[n] 必须是
│ registry: │ by_issue: 争点→编号 │ 登记引文逐字摘录
│ [n] → 案号/块ID/ │ statutes: 法条现行原文 │ · 证据不足显式声明
│ seg/逐字quote/URL/ │ │ · 不得编造案例案号法条
│ 案由/重排理由 │ ▼
└─────────▲──────────┘ 生成的报告
│ │
│ 确定性算法逐字比对(非模型判断) ▼
└───────────────────────────── ┌─────────────────────┐
│ check · 三道校验 │
│ ① [n] 编号在登记表内 │
│ ② 「引文」⊆ 登记quote │
│ (忽略空白;相似度 │
│ SequenceMatcher) │
│ ③ 登记引文无闲置 │
└──────────┬──────────┘
未通过+regen<2 ──► write(附问题清单)
通过 ──► done(徽章:完整 ✓)
耗尽 ──► 徽章:部分降级 + 问题清单
机制基础:登记表 quote 构造时就取自块文本的逐字子串,因此"②逐字一致"这道确定性校验在数学上可 100% 拦截编造引文(溯源优先原则)。
3.5 Token 指标管道(需求 5 数据结构)
任意 LLM 调用面 metrics.emit()(同步、不阻塞调用方)
chat / chat_json / │
embed / web-search-pro ──usage────────►│
├──► XADD wf:llm_tokens
│ MAXLEN ~100000(近似裁剪)
│ EntryID + 9 字段:
│ ts/user_id/matter_id/stage/
│ name/model/tokens_in/
│ tokens_out/total
│ │
│ ▼ 每次成功后 30s 节流
├──► SQLite token_usage(本地账本,
│ redis_ok=0/1 标记是否已入 Stream)
│ │
│ Redis 恢复后:
│ pending(redis_ok=0) ──补发XADD──► 置 ok
│ │
▼ ▼
读取接口:XREVRANGE(新→旧,可按 user 过滤)
聚合接口:SQLite GROUP BY stage / model
双写设计:Redis Stream 是需求要求的消息队列语义(可回放/可 XREAD 实时消费),SQLite 账本保证 Redis 掉线期间指标不丢(补发机制),同时支撑聚合统计。
3.6 实时互联网采集管道(流程图)
CollectScheduler(后台 asyncio.Task,启动延迟 10s)
├─ 连续模式(默认):一轮全主题大清扫 → 冷却 21600s(6h) → 下一轮,永不停轮
└─ 间隔模式:AUTO_COLLECT_MINUTES 定时
│ 每轮 = 14 个主题 × 轮换查询变体(kv:collect_topic_index 记游标)
│ 单主题 try/except:失败只记数,不中断本轮
▼ 每主题
┌───────────────┐ ┌──────────────────┐ ┌───────────────┐ ┌──────────┐
│ web-search-pro │──►│ GLM 结构化整理 │──►│ CaseRecord │──►│ 同一入库 │
│ POST /tools │ │(提示词禁编造, │ │ 案号防伪:无案号│ │ 管道 │
│ 实时联网搜索 │ │ 忠实搜索结果) │ │ → WEB-md5(url)│ │ add_case:│
│ title/link/ │ │ 截断JSON → │ └───────────────┘ │ 分块→向量 │
│ content │ │ _salvage_items │ │ →BM25/图谱│
└───────────────┘ │ 对象级抢救 │ │ →持久化 │
└──────────────────┘ └──────────┘
│
└──► collected_items 表(来源/URL/摘要/raw,全程可溯源)
采集本身消耗的 token 也 emit 到 wf:llm_tokens(stage=collect.*)
采集数据与 17 个种子案例走完全相同的入库管道(store.add_case() 单一入口),采集结果立即可被四路召回命中、进入分析证据。
3.7 对话式入口与中间思考展示(交互设计图)
POST /api/v1/chat {user_id, message, attachment?}
│
▼
GLM 意图分类(chat.route,输出 JSON)
│
├─ create_task ──► 附件全文+消息组装为案情 → 建任务 → engine.start
├─ pause/resume ──► 作用于该用户最近任务(engine 控制接口,语义同 REST)
├─ cancel/status ──► 同上;status 汇报 阶段/检查点/token 摘要
├─ collect ──────► collect_from_internet(query) → 播报入库结果
└─ qa ──────────► store.search_cases(top5) + 附件节选
→ QA_PROMPT 受约束 JSON 回答(≤300字,附案号)
中间思考展示链路(需求:实时播报):
LLM reasoning_content ─► record_thinking() ─┬─ state["thinking"][key](随检查点持久化,
│ workflow 接口可见,≤600字)
└─ events 表 agent_thinking 事件(≤400字)
│
前端 2.5s 轮询 /tasks/{id}/events ─────────────────┘
eventToMsg() 将 阶段完成/暂停/恢复/思考/失败 翻译为对话流气泡 💭
3.8 数据模型与持久化(存储设计图)
SQLite lawsight.db(单连接+RLock 线程安全) CaseStore 内存索引 + 文件
┌───────────────┐ 1:N ┌────────────────┐ ┌──────────────────────────┐
│ tasks │──────►│ checkpoints │ │ cases{案号→CaseRecord} │
│ task_id PK │ │ seq/stage/ │ │ chunks{cid→EvidenceChunk}│
│ user_id/matter │ │ status/state │ │ laws{法条名+条号→LawArticle}│
│ status/stage/ │ │ (TaskState全量 │ │ bm25(倒排+df+avgdl) │
│ report/error │ │ JSON 快照) │ │ vectors{cid→2048维} │
└───────┬───────┘ └────────────────┘ │ statute_index 法条→块集合 │
│ 1:N ┌────────────────┐ │ cause_graph 案由→案例集 │
┌───────▼───────┐ │ events │ └────────────┬─────────────┘
│ token_usage │ │ (审计流,chat │ │ 增量持久化
│ 本地账本 │ │ 播报的数据源) │ case_store.json(案例+法条主数据)
│ redis_ok 标记 │ └────────────────┘ case_vectors.npz(向量,重启零成本恢复)
└───────────────┘ ┌────────────────┐
┌───────────────┐ │ collected_items │ TaskState(随检查点持久化的内存态):
│ kv │ │ 采集溯源账本 │ 争点/计划/召回/证据包/报告/轮次/
│ ctl:{task_id} │ └────────────────┘ thinking/stage_timings/history/
│ 控制标记 │ next_stage(恢复游标)
│ 采集主题游标 │
└───────────────┘