- Issue9: client.py 由同步 httpx.Client 全异步化 - LLMClient Protocol / HttpLLMClient.chat → async;httpx.AsyncClient + asyncio.sleep 退避 - __enter__/__exit__ → __aenter__/__aexit__(async with 生命周期闭环) - engine.py chat/chat_structured/_call 全部 async + await - FakeLLMClient.chat → async;测试用 anyio pytest 插件转换(engine 32 + client 10 用例) - 同步 inference-engine spec 与 milestone3 review 的 httpx 描述 - 全量 182 passed / 100.00%(987 stmts/252 br)
70 lines
5.8 KiB
Markdown
70 lines
5.8 KiB
Markdown
# 里程碑 3.1 InferenceEngine 交付报告
|
||
|
||
## 1. 概述
|
||
|
||
完成 `agent-runtime-design.md` §2 定义的推理引擎(InferenceEngine),为多 Agent 协作提供统一 LLM 调用入口:模型选择/降级、重试、超时、Token 管理、Prompt 注册表与结构化输出。全程 TDD、零真实网络、覆盖率红线 100% 达成。
|
||
|
||
- **评审对象**:`docs` 阶段(spec + plan)经 plan-design-review 批准后,实施提交范围 `2fbff07..HEAD`(共 6 个 commit)
|
||
- **验收结论**:With fixes → 2 项 Important 已修复 → 重新验证 **Ready**(117 passed / 100% 覆盖)
|
||
|
||
## 2. 交付内容
|
||
|
||
| 模块 | 文件 | 说明 |
|
||
|------|------|------|
|
||
| 数据模型 | `src/genesis/inference/types.py` | TokenUsage / ChatMessage / ChatResult / StructuredResult / Prompt |
|
||
| 异常体系 | `src/genesis/inference/exceptions.py` | LLMError 五类:Network / Timeout / NotConfigured / ResponseError |
|
||
| Token 估算 | `src/genesis/inference/token.py` | approximate(4 字符≈1 token)+ tiktoken 可选懒加载回落 |
|
||
| Prompt 注册表 | `src/genesis/inference/prompt_registry.py` | register / get / list_versions / render(jinja2) |
|
||
| LLM 客户端 | `src/genesis/inference/client.py` | LLMClient Protocol + HttpLLMClient(OpenAI 兼容) |
|
||
| 编排引擎 | `src/genesis/inference/engine.py` | InferenceEngine:chat / chat_structured |
|
||
| 测试 | `tests/test_inference_{types,errors,token,prompt_registry,client,engine}.py` + `tests/inference_helpers.py` | 全部离线用例 |
|
||
|
||
## 3. 关键设计决策
|
||
|
||
1. **httpx 选型**:`httpx.AsyncClient(timeout=..., transport=...)` 支持 transport 注入(MockTransport 离线测试);5xx/网络错误指数退避重试(1s/3s/7s),超时→LLMTimeoutError,4xx 不重试→LLMNetworkError,2xx 结构损坏→LLMResponseError,3xx 不误判成功。(T8 整改后为异步 AsyncClient + asyncio.sleep 退避 + async with 生命周期)
|
||
2. **结构化重试语义**:`chat_structured` 对 JSON 解析失败带错误信息重试(retry_count+1),重试成功仍为 `status="ok"`(fallback 语义保留给模型降级);最终失败返回 `parse_error` + raw_text,不抛异常(spec §3.4 验收 3)。
|
||
3. **异常折叠边界**:`chat` 失败降级备用模型(status=fallback),双模型全败 status=failed;仅折叠 LLMError,非 LLM 异常照常冒出。
|
||
4. **显式延后(非目标)**:
|
||
- `chat_structured` 不做模型降级——plan 明确「生产建议显式传主模型,实现已足够 v1」(plan L1142),结构化重试固定主模型。
|
||
- Prompt 目录加载(`prompts/{name}/{version}.txt`)为 v2,v1 仅代码内注册。
|
||
- LLMResponseError 在 v1.0 spec 已定义但无触发点,最终评审后补齐(见 §5)。
|
||
|
||
## 4. 测试与验证
|
||
|
||
- 聚焦测试:各模块 RED→GREEN 全绿(types 8 / token 7 / registry 6 +1 / client 9 / engine 16)
|
||
- 全量回归:`python -m pytest -q` → **117 passed / 覆盖率 100.00%(785 stmts / 186 br)**,`fail_under=99` 达成
|
||
- 零真实网络:单测全部注入 FakeLLMClient / httpx.MockTransport;`DEEPSEEK_API_KEY` 不落代码
|
||
- 基线不回退:既有 parsers / data_models / config 全绿保持
|
||
|
||
## 5. 最终评审(base 2fbff07 → 8bc5e5e)与修正
|
||
|
||
评审判定 **With fixes**(无 Critical;2 项 Important;若干 Minor):
|
||
|
||
1. **[已修复] 响应结构异常逃逸**:2xx 畸形响应(非 JSON / 缺 choices 字段)抛裸 `KeyError/JSONDecodeError`,逃出 engine 的 `except LLMError`,与「不把异常抛给调用方」承诺冲突。修复:client 内捕获 `(json.JSONDecodeError, KeyError, IndexError, TypeError)` 后 `raise LLMResponseError`(spec §3.8 既有类型首次被触发,闭环 api-design §7 LLM_PARSE_ERROR 来源列)。TDD:新增 `test_chat_3xx_no_retry` 与 `test_chat_malformed_response_raises_llm_response_error`。
|
||
2. **[已修复] 3xx 误判成功**:httpx 默认不跟随重定向,301/302 落入成功路径。修复:显式 `200 <= resp.status_code < 300` 判定,非 2xx 一律 LLMNetworkError(不重试)。
|
||
3. **[确认为有意] chat_structured 无模型降级**:plan 已声明 v1 单模型链,生产显式传主模型,非回归缺陷。
|
||
4. **[Minor 挂账 backlog]**:`chat` 失败仅保留最后一次模型错误信息;`__init__.py` 缺尾换行;prompt_registry 模块级 jinja2 导入冗余;chat_structured 未做 token 超限检测;HttpLLMClient 无 `__enter__/__exit__`(调用方负责关闭生命周期)。
|
||
|
||
修复提交 `14062d1`(fix: 客户端响应结构损坏归入 LLMResponseError;3xx 不再误判成功)。
|
||
|
||
## 6. 提交清单
|
||
|
||
```
|
||
c2fad77 feat: 推理引擎数据模型与异常层(types/exceptions)及 httpx/jinja2 依赖
|
||
71d38b9 feat: Token 估算(approximate 内置 / tiktoken 可选回落)
|
||
59c1ea7 feat: PromptRegistry 注册/版本/渲染(jinja2)
|
||
add7b15 feat: HttpLLMClient(httpx + 重试退避/超时/鉴权)
|
||
8bc5e5e feat: InferenceEngine chat/chat_structured 全流程 + 文档补丁(补丁1/2/3)
|
||
14062d1 fix: 客户端响应结构损坏归入 LLMResponseError;3xx 不再误判成功
|
||
```
|
||
|
||
## 7. 涉及文件
|
||
|
||
- 新建:`src/genesis/inference/{types,exceptions,token,prompt_registry,client,engine}.py`、`tests/inference_helpers.py`、`tests/test_inference_{types,errors,token,prompt_registry,client,engine}.py`
|
||
- 修改:`src/genesis/inference/__init__.py`、`pyproject.toml`(httpx/jinja2 依赖)、`docs/agent-runtime-design.md`、`docs/api-design.md`、`docs/config-design.md`
|
||
- 文档:spec `docs/superpowers/specs/2026-08-09-inference-engine-design.md`、plan `docs/superpowers/plans/2026-08-09-inference-engine.md`
|
||
|
||
## 8. 后续工作
|
||
|
||
- 接入 Parser Agent 自由记述型结构化(待后续里程碑)
|
||
- LLMResponseError 已闭环,可为 engine 增加按错误分类的重试选项(api §7 retry/skip/abort UX) |