From 25fc472d9b6e8b0a75efa794ae7eb29addeeeeae Mon Sep 17 00:00:00 2001 From: lhl Date: Wed, 12 Aug 2026 09:32:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(inference):=20chat=5Fstructured=20?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=20jsonschema=20=E7=9C=9F=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=EF=BC=88T1=20=E6=9E=B6=E6=9E=84=E5=AE=A1=E6=9F=A5=E6=95=B4?= =?UTF-8?q?=E6=94=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pyproject.toml 新增 jsonschema>=4.23 依赖 - engine.py: json.loads 后 jsonschema.validate,校验失败按解析失败重试(带错误信息) - 新增 3 用例:违规重试成功/耗尽 parse_error 含详情/合法一次通过 - 全量 163 passed / 100.00% 覆盖(936 stmts/238 br),fail_under=99 达标 --- _AI_USAGE_LOG.md | 2 + docs/architecture-eng-review.md | 185 ++++++++++++++++++++++++++++++++ pyproject.toml | 1 + src/genesis/inference/engine.py | 10 ++ tests/test_inference_engine.py | 43 ++++++++ 5 files changed, 241 insertions(+) create mode 100644 docs/architecture-eng-review.md diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index c951923..72ef869 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -69,3 +69,5 @@ | 2026-08-10 | 测试验证 | Phase3 Task5 实现:真实样本 Word 解析集成测试。tests/test_real_samples.py 追加 4 用例(保留现有 4 个 Excel 用例与 _x 辅助):模板→7 H1(はじめに…バッチ一覧)+section:introduction/doc_title 占位符+template_start 书签;记入規則→category=write/file_type=word/# 1. 機能一覧の書き方/- 機能ID は F001 から連番で付与する。(・前缀双通道检测);做成説明書→category=write/# 2. 機能一覧;全量组装→tables 非空+template 非空+rule_docs==2 且全 write;聚焦测试 8 passed 无 skip;pytest 全量 160 passed 覆盖 100.00%(929 stmts/236 br),fail_under=99 达标 | tests/test_real_samples.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free | | 2026-08-10 | 测试验证 | Phase3 Word 解析最终评审收尾:3 文件补尾随换行、test_real_samples import 排序、SourceParser docstring 契约优先级注释 | src/genesis/parsers/source_aggregator.py, tests/test_source_aggregator.py, tests/test_real_samples.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free | | 2026-08-10 | 测试验证 | Phase3 Word 解析里程碑评审报告:docs/milestone3-word-parser-review.md(5 任务 task review 全 Approved + 最终 whole-branch review With fixes 闭环;160 passed / 100.00% 分支覆盖 929 stmts/236 br;Pre-Flight 裁决 3 项 + 覆盖补齐 4 项记录;deferred 遗留 6 项移交后续) | docs/milestone3-word-parser-review.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free | +| 2026-08-11 | 架构设计 | 整体架构工程审查(plan-eng-review,SCOPE_REDUCED 模式):Step0 裁定移除 Qdrant 抽象+Redis 双实现(单一 Chroma+InMemory);Section1 架构 7 issue(jsonschema 真校验/解析降级链/cancelled+resume 状态/引擎层注入防护/删死配置/v1 rerank/记忆完整实现);Section2 代码质量 3 issue(DRY helper/全异步化/局部变量);Section4 性能 2 issue(CJK token 估算/串行约束文档化);Outside Voice 8 项全接受(文档同步/bge-m3/resolver/黄金集/DataGate 机制化/QA 护栏+fallback/任务持久化/docx 原型提前);产出 17 项 Implementation Tasks JSONL;报告 docs/architecture-eng-review.md | docs/architecture-eng-review.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free | +| 2026-08-11 | Agent 实现 | T1(架构审查整改):chat_structured 接入 jsonschema 真校验。pyproject.toml 新增 jsonschema>=4.23 依赖;engine.py 在 json.loads 后调用 jsonschema.validate(schema 非空时),校验失败按解析失败重试(新增 except jsonschema.ValidationError 分支,带校验错误信息重试);新增 3 用例(违规重试成功/违规耗尽 parse_error 含校验详情/合法一次通过);TDD 验证 RED({'a':'not_a_number'} 未被拒直接返回)→ GREEN(聚焦 4 passed)→ 全量 163 passed 覆盖 100.00%(936 stmts/238 br),fail_under=99 达标 | pyproject.toml, src/genesis/inference/engine.py, tests/test_inference_engine.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free | diff --git a/docs/architecture-eng-review.md b/docs/architecture-eng-review.md new file mode 100644 index 0000000..75bb294 --- /dev/null +++ b/docs/architecture-eng-review.md @@ -0,0 +1,185 @@ +# 整体架构工程审查报告(plan-eng-review) + +> 版本: v1.0 | 日期: 2026-08-11 | 状态: 已裁定 +> +> 审查技能: gstack `plan-eng-review`(version 1.0.0)| 审查对象: 整体架构全景(docs/design.md + agent-runtime-design.md + rag-layer-design.md + api-design.md + implementation-plan.md)| 当前进度: Phase3 Word 解析收尾(160 tests / 100%) + +--- + +## 1. 审查范围与裁定摘要 + +本次审查锁定**整体架构全景**,目标为后续 Phase4-6(RAG / Impact / Writer / Web UI)实施提供架构定论。 + +| 审查阶段 | 结果 | +|---------|------| +| Step 0 Scope Challenge | **范围缩减**:移除 Qdrant 切换抽象 + Redis/Valkey 双实现(单一 Chroma + InMemoryQueue)| +| Section 1 Architecture | 7 个 issue,全部裁定 | +| Section 2 Code Quality | 3 个 issue,全部裁定 | +| Section 3 Test Review | 覆盖图完成,无独立缺口(随架构裁定覆盖)| +| Section 4 Performance | 2 个 issue,全部裁定 | +| Outside Voice | 独立挑战完成,8 项全部接受 | + +--- + +## 2. Step 0: Scope Challenge 裁定 + +**复杂度检查触发**(全架构远超 8 文件/2 服务阈值),发现 2 处过度设计: + +| # | 过度设计点 | 裁定 | +|---|-----------|------| +| S1 | Qdrant 可切换抽象(rag-layer §9 StorageAdapter)| **移除** — v1 单机竞赛场景 Chroma 嵌入式足够(搜索验证:<10 万向量 Chroma 正确默认)| +| S2 | Redis/Valkey TaskQueue 双实现(api-design §5)| **移除** — v1 InMemoryQueue 满足演示需求,抽象接口保留但双实现不建 | + +> Search Check 结论:手写 RAG(chromadb+rank_bm25+RRF)与 2026 主流实践一致;Chroma 嵌入式对小规模规则库正确。 + +--- + +## 3. Section 1: Architecture Review(7 issue 全裁定) + +| # | 严重度 | 置信度 | 发现 | 裁定 | +|---|--------|--------|------|------| +| I1 | P1 | 9/10 | `chat_structured` 无真正 schema 校验(engine.py:160 直接 json.loads 返回)| **加 jsonschema 真校验**(失败→带错误重试→parse_error+校验详情)| +| I2 | P2 | 8/10 | 解析重试路径硬编码首选模型,从不降级(engine.py:155)| **解析重试也走降级链**(primary→fallback)| +| I3 | P1 | 7/10 | 状态机 8 状态无 cancelled/aborted 定义,但 api 有 cancel 端点 | **加 cancelled+resume 状态**(cancelled 终态 + resume 回到中断前)| +| I4 | P2 | 7/10 | design §8.1 宣称「引擎统一注入防护」但 engine.py 无实现 | **引擎层统一防护**(边界包裹+系统指令恒定性,全 Agent 生效)| +| I5 | P2 | 8/10 | config.py 残留 QdrantStoreConfig(L95-103)/ redis_url(L38-41)死配置 | **删死配置+同步文档** | +| I6 | P2 | 7/10 | RAG 设计缺 rerank 精排(2026 主流实践为 向量→rerank→精排)| **v1 加 rerank**(bge-reranker-v2-m3)| +| I7 | P3 | 5/10 | 三层记忆 + DataGate + AgentState 对单进程场景可能过度 | **完整实现**(用户认为记忆系统是核心价值)| + +## 4. Section 2: Code Quality Review(3 issue 全裁定) + +| # | 严重度 | 置信度 | 发现 | 裁定 | +|---|--------|--------|------|------| +| I8 | P2 | 9/10 | SourceParser 三重复制校验(source_aggregator.py:39-66 DRY 违规)| **提取 _validate_path helper** | +| I9 | P2 | 8/10 | client.py:82 time.sleep 阻塞 asyncio 任务池 + client 生命周期未闭环 | **全异步化**(AsyncClient + asyncio.sleep + async 上下文)| +| I10 | P3 | 6/10 | chat_structured 重复调用 _model_names(None)[0](4 处)| **随 I2 一并修** | + +## 5. Section 3: Test Review + +已实现部分 **160 tests / 100% 覆盖**(fail_under=99 门禁生效),FakeLLMClient 注入模式成熟。覆盖图确认关键测试缺口集中在**已裁定架构变更**(jsonschema/降级链/注入防护/异步化需测试先行)与后续阶段(RAG chunker/检索 eval、状态机、渲染链路 E2E)。 + +## 6. Section 4: Performance Review(2 issue 全裁定) + +| # | 严重度 | 置信度 | 发现 | 裁定 | +|---|--------|--------|------|------| +| I11 | P2 | 7/10 | token.py:18 cl100k_base 对 CJK 严重低估 token 数 → 裁剪策略失效 | **CJK 保守估算**(模型 tokenizer 优先,退化 CJK 字符×1.5)| +| I14 | P2 | 7/10 | Writer 逐章串行生成 3-5 分钟 | **保持串行+文档约束**(章间引用需前章摘要,并行复杂;UI 预估时长提示)| + +## 7. Outside Voice(独立挑战,8 项全接受) + +| # | 发现 | 裁定 | +|---|------|------| +| OV1 | 审查决策未写回执行文档(rag-layer/api-design/runtime/plan 仍含双实现描述,Phase4 会重建放弃的抽象)| 接受:T5 同步修订四处文档 | +| OV2 | bge-small-zh-v1.5 面向中文,实际语料为日文 | 接受:T11 切换 bge-m3+日文样本验证 | +| OV3 | 可追溯性标准未操作性定义(URI 格式不一致、无 resolver、QA#8 可作弊)| 接受:T12 统一 URI+resolver+强验证 | +| OV4 | 成功标准无量度(无黄金集/评分器)| 接受:T13 建立黄金集+评分器 | +| OV5 | DataGate 是原则非机制,1000 行 Excel 上下文爆炸 | 接受:T14 机制化+规模测试前置 | +| OV6 | QA 循环无边界 + 自校验盲区(DeepSeek 检查 DeepSeek)| 接受:T15 护栏+QA 走 fallback_model | +| OV7 | 崩溃恢复只到会话级,任务层丢数据 | 接受:T16 任务级持久化 | +| OV8 | 最难成功标准(格式精度)排关键路径末尾 | 接受:T17 docx 注入原型提前 | + +--- + +## 8. Required Outputs + +### 8.1 NOT in scope(本次审查明确延后) + +| 项 | 延后理由 | +|----|---------| +| Qdrant/QdrantAdapter | 与 S1 裁定一致,移除(v2 触发项)| +| Redis/Valkey Queue 实现 | 与 S2 裁定一致,接口保留(v2 触发项)| +| LLM 调用缓存 | design §9.2 已列 v2,识别为有意延期 | +| 成本/限流监控 | design §9.1 v2 预留 | +| Type C 参考设计文档 RAG | rag-layer §1.1 已声明可选增强,初期聚焦 Type A+B | +| 多用户并发隔离 | v1 单用户演示,多用户隔离基线保留(§8.2)| + +### 8.2 What already exists(设计复用核查) + +| 已实现 | 复用情况 | +|--------|---------| +| InferenceEngine(M3.1)| ✅ 设计直接复用;需按 I1/I2/I4/I9 加固 | +| Excel/Word/混合 Parser(Phase2/2.5/3)| ✅ 设计直接复用;I8 微重构 | +| PromptRegistry / token 估算 | ✅ 复用;I11 修正估算 | +| 7 个脱敏真实样本(samples/)| ✅ T13 黄金集基础 | +| config 三 yaml + env 覆盖 | ✅ 复用;I5 清理死配置 | + +### 8.3 失败模式(关键路径) + +| 路径 | 失败场景 | 测试覆盖 | 错误处理 | 用户可见性 | +|------|---------|---------|----------|-----------| +| chat_structured 输出不合 schema | LLM 返回缺字段 JSON → 消费方 KeyError | I1 修复后新增 | I1 修复后 parse_error | ✅ 重试后显示失败 | +| 解析重试主模型弱 | 重试 2 次全败 → 无降级 | I2 修复后新增 | I2 修复后 fallback | ✅ 降级事件可见 | +| 用户取消生成 | 状态悬置无定义 | I3 修复后新增 | I3 修复后 cancelled | ✅ cancel 端点语义明确 | +| 恶意规则文档注入 | 规则含「忽略以上指令」→ 污染输出 | I4 修复后新增 | I4 修复后引擎级阻断 | ⚠️ 需测试验证 | +| 日文长文本超上下文 | 裁剪低估 → API 超限拒绝 → 重试循环 | I11 修复后新增 | I11 修复后裁剪准确 | ✅ 重试机制 | +| 服务重启丢进行中任务 | 中断后重复章节或卡死 | OV7(T16) 后新增 | 需任务落盘 | ⚠️ 当前**静默失败** | + +### 8.4 Worktree 并行化策略 + +``` +Lane A(前端+编排):T3 状态机 → T14 DataGate → T16 任务持久化(共享 orchestrator/) +Lane B(RAG 独立) :T6 rerank → T11 bge-m3(共享 rag/,不与 A 冲突) +Lane C(推理加固) :T1 jsonschema → T2 降级链 → T4 注入防护 → T8 异步 → T9 token(共享 inference/,串行) +Lane D(基础设施) :T7 helper / T5 死配置 / T10 文档 / T12 resolver / T13 黄金集 / T15 QA / T17 原型 +``` + +执行序:**C(加固核心)先于一切** → A + B 并行 → D 穿插。Lane C 与 A 共享引擎层,建议 C 完成后 A 再启动。 + +--- + +## 9. Implementation Tasks(17 项) + +全部任务已输出至 `~/.gstack/projects/unknown/tasks-eng-review-20260811-160000.jsonl`(/autoplan 可聚合)。 + +- [ ] **T1 (P1, human: ~2h / CC: ~20min)** — inference — chat_structured 接入 jsonschema 真校验 +- [ ] **T2 (P1, human: ~1.5h / CC: ~15min)** — inference — 解析重试降级链 + 模型名局部变量 +- [ ] **T3 (P1, human: ~3h / CC: ~40min)** — orchestrator — 状态机 cancelled+resume +- [ ] **T4 (P1, human: ~3h / CC: ~30min)** — inference — 引擎层统一注入防护 +- [ ] **T5 (P2, human: ~1h / CC: ~10min)** — config — 删死配置+同步文档(含 OV1) +- [ ] **T6 (P2, human: ~4h / CC: ~45min)** — rag — v1 rerank 精排 +- [ ] **T7 (P2, human: ~30min / CC: ~5min)** — parsers — 提取 _validate_path helper +- [ ] **T8 (P1, human: ~5h / CC: ~1h)** — inference — LLM 客户端全异步化 +- [ ] **T9 (P2, human: ~1h / CC: ~15min)** — inference — CJK 保守 token 估算 +- [ ] **T10 (P3, human: ~1h / CC: ~10min)** — documentation — Writer 串行约束写回 +- [ ] **T11 (P2, human: ~3h / CC: ~30min)** — rag — bge-m3 + 日文样本验证(OV2) +- [ ] **T12 (P1, human: ~6h / CC: ~1.5h)** — provenance — URI 统一+resolver(OV3) +- [ ] **T13 (P1, human: ~8h / CC: ~2h)** — eval — 黄金集+评分器(OV4) +- [ ] **T14 (P2, human: ~5h / CC: ~1h)** — orchestrator — DataGate 机制化(OV5) +- [ ] **T15 (P2, human: ~3h / CC: ~40min)** — qa — QA 护栏+fallback 路由(OV6) +- [ ] **T16 (P2, human: ~6h / CC: ~1.5h)** — orchestrator — 任务级持久化(OV7) +- [ ] **T17 (P1, human: ~6h / CC: ~1.5h)** — writer — docx 注入原型提前(OV8) + +--- + +## 10. 完成摘要(Completion Summary) + +- Step 0 Scope Challenge: **范围缩减**(移除 2 处双实现抽象) +- Architecture Review: 7 issues found(全部裁定) +- Code Quality Review: 3 issues found(全部裁定) +- Test Review: 覆盖图已产出,0 独立缺口(随架构裁定覆盖) +- Performance Review: 2 issues found(全部裁定) +- NOT in scope: 已写入(§8.1) +- What already exists: 已写入(§8.2) +- TODOS.md: N/A(项目无此文件;17 项任务已入 JSONL) +- Failure modes: 6 条关键路径记录,1 处**临界缺口**(重启丢任务静默失败 → T16 修复) +- Outside voice: 已运行(Claude 子代理),8 项全部接受 +- Parallelization: 4 lanes,C→(A+B) 并行,D 穿插 +- Lake Score: 17/17 推荐选择完整方案 + +--- + +## GSTACK REVIEW REPORT + +| Review | Trigger | Why | Runs | Status | Findings | +|--------|---------|-----|------|--------|----------| +| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — | +| Codex Review | `/codex review` | Independent 2nd opinion | 0 | — | — | +| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | ISSUES_OPEN | 11 issues + 8 outside voice | +| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | — | +| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | — | — | + +- **CROSS-MODEL:** Outside voice(Claude 子代理)与主审查交叉验证——OV1 为已裁定项的执行监督,OV2/3/4/5/6/7/8 为互补新发现,无实质分歧。 +- **VERDICT:** ENG REVIEW 完成(11 issues + 8 findings 全部裁定,待实施后复核)。**eng review not yet CLEAR — 需任务实施后重跑确认。** + +**UNRESOLVED DECISIONS:** +- 无(全部 19 项裁定完成) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5b9bc5a..70f6e5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "python-docx>=1.0", "httpx>=0.28", "jinja2>=3.1", + "jsonschema>=4.23", ] [project.optional-dependencies] diff --git a/src/genesis/inference/engine.py b/src/genesis/inference/engine.py index 1a9fc8a..c841d23 100644 --- a/src/genesis/inference/engine.py +++ b/src/genesis/inference/engine.py @@ -4,6 +4,8 @@ import json import time from typing import Any, Callable +import jsonschema + from .client import LLMClient from .exceptions import LLMError from .prompt_registry import PromptRegistry @@ -158,6 +160,9 @@ class InferenceEngine: ) last_raw = text data = json.loads(text) + if schema: + # 真 schema 校验:不合 schema 时按解析失败重试(T1) + jsonschema.validate(instance=data, schema=schema) return StructuredResult( data=data, raw_text=text, parse_attempts=attempts, model=self._model_names(None)[0], @@ -172,6 +177,11 @@ class InferenceEngine: last_error_code = "LLM_PARSE_ERROR" # 带错误信息重试 base_rendered = base_rendered + f"\n\n上次解析失败:{exc}。请重新输出合法 JSON。" + except jsonschema.ValidationError as exc: + last_error = f"校验失败: {exc.message}" + last_error_code = "LLM_PARSE_ERROR" + # 带校验错误信息重试 + base_rendered = base_rendered + f"\n\n上次校验失败:{exc.message}。请重新输出符合 JSON Schema 的 JSON。" except LLMError as exc: return StructuredResult( data={}, raw_text="", parse_attempts=attempts, diff --git a/tests/test_inference_engine.py b/tests/test_inference_engine.py index 8737f92..a880d75 100644 --- a/tests/test_inference_engine.py +++ b/tests/test_inference_engine.py @@ -318,6 +318,49 @@ def test_chat_structured_empty_schema_no_hint(): assert r.status == "ok" and r.data == {"v": True} +# ---------- T1: chat_structured 真 schema 校验(jsonschema) ---------- + +def test_chat_structured_schema_violation_retries(): + """返回不合 schema 的 JSON 时带错误信息重试;第二次合法 → ok。""" + client = FakeLLMClient([("ok", '{"a": "not_a_number"}'), ("ok", '{"a": 2}')]) + eng = make_engine(client) + r = eng.chat_structured( + session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"), + variables={}, + schema={"type": "object", "properties": {"a": {"type": "number"}}, "required": ["a"]}, + ) + assert r.status == "ok" and r.data == {"a": 2} and r.parse_attempts == 2 + # 第二次调用带上次校验错误信息(重试提示) + assert "校验失败" in client.calls[1]["messages"][0] + + +def test_chat_structured_schema_violation_parse_error(): + """全部返回不合 schema 的 JSON → parse_error,error 含校验详情。""" + client = FakeLLMClient([("ok", '{"a": "bad"}'), ("ok", '{"a": "bad"}')]) + eng = make_engine(client) + r = eng.chat_structured( + session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"), + variables={}, + schema={"type": "object", "properties": {"a": {"type": "number"}}, "required": ["a"]}, + retry_count=1, + ) + assert r.status == "parse_error" + assert "校验失败" in (r.error or "") + assert r.parse_attempts == 2 + + +def test_chat_structured_schema_valid_passes_without_retry(): + """返回合法 JSON 时一次通过,不触发重试。""" + client = FakeLLMClient([("ok", '{"a": 1}')]) + eng = make_engine(client) + r = eng.chat_structured( + session_id="s1", prompt=Prompt(name="p", version="v1", template="提取"), + variables={}, + schema={"type": "object", "properties": {"a": {"type": "number"}}, "required": ["a"]}, + ) + assert r.status == "ok" and r.data == {"a": 1} and r.parse_attempts == 1 + + def test_chat_truncation_callback_returns_none_keeps_variables(): """truncate_cb 返回 None 时回退原 variables(覆盖 new_vars is None 分支)。""" def truncate_cb(prompt_text, variables):