Files
2026Technology-Competition/docs/superpowers/specs/2026-08-09-inference-engine-design.md
T
lhl 8239a37a99 feat(inference): LLM 客户端全异步化(T8 架构审查整改)
- 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)
2026-08-12 09:50:37 +08:00

212 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 里程碑 3.1InferenceEngine(推理引擎)设计
- 日期:2026-08-09
- 状态:已批准(用户 2026-08-09 确认;plan-design-review 已执行,无 UI scope
- 目的:落地 `implementation-plan` 阶段 1.6「LLM Client 抽象化」扩展为推理引擎(agent-runtime §2),使所有 Agent 的 LLM 调用经统一入口
## 0. 审查结论(plan-design-review
- **UI 维度**:无 UI 范围(纯后端基础设施),designer's-eye 审查不适用(按 skill 退出条款)
- **一致性审查**:接口/模型/退避/Token/PromptRegistry/环境变量 与 agent-runtime §2、config-design §2/§4、api-design §7 全部对齐
- **审查产出 2 项补丁(已并入本 spec §3.8 / §3.9**
- 补丁 1`StructuredResult` 增加 `status: Literal["ok","parse_error","failed"]` 字段(基线 runtime §2.2 无此字段)
- 补丁 2:错误码 `LLM_NOT_CONFIGURED`503)补入 api-design §7 错误码表(config-design §7 有定义但 api-design 表缺失)
## 1. 背景与触发条件
- `implementation-plan.md` 阶段 1.6 要求「LLM Client 抽象化」;`agent-runtime-design.md` §2 将该抽象扩展为完整的推理引擎(统一 LLM 调用入口、降级、重试、Token 管理、Prompt 版本化)
- 当前仓库**尚无任何 LLM 客户端实现**4 个 AgentParser / Impact / Writer / QA)均等待推理引擎基盘
- 技术选型已定:原生 HTTP(不使用 SDK 封装);HTTP 客户端选择 **httpx**pyproject 新增依赖),采用**异步 `AsyncClient`**(T8 架构审查整改:由同步 Client 全异步化,适配编排层 asyncio 任务池,退避用 asyncio.sleepasync with 生命周期闭环)
## 2. 目标
- 提供 `InferenceEngine.chat()` / `chat_structured()` 统一 LLM 入口,接口与 agent-runtime §2.2 一致
- 完整实现:模型选择优先级、重试(指数退避 1s/3s/7s)、超时(默认 60s)、主模型→备用模型降级、Token 估算与超限裁剪回调、PromptRegistry 注册/渲染/版本化
- **可注入 HTTP 适配器**:单测全路径离线运行(注入 FakeAdapter),零真实网络依赖;真实网络走可选在线用例
- 结构化输出失败不抛异常:以 `status="parse_error"` + 原始文本返回,调用方决定(对齐 api-design §7 LLM_PARSE_ERROR 语义)
## 3. 设计
### 3.1 模块结构
```
src/genesis/inference/
├── __init__.py # 导出 InferenceEngine / 数据类 / 异常
├── engine.py # InferenceEnginechat / chat_structured
├── client.py # LLMClient 抽象 + HttpLLMClient(httpx) + 重试/退避
├── token.py # token 估算(approximatetiktoken 可选)+ 超限回调
├── prompt_registry.py # PromptRegistryregister/get/render/版本化)
├── types.py # TokenUsage / ChatMessage / ChatResult / StructuredResult / Prompt
└── exceptions.py # LLMError 层级(LLMTimeoutError / LLMNotConfiguredError
```
### 3.2 数据模型(`types.py`
```python
@dataclass
class TokenUsage:
input_tokens: int = 0
output_tokens: int = 0
@dataclass
class ChatMessage:
role: Literal["system", "user", "assistant"]
content: str
@dataclass
class ChatResult:
text: str
model: str
prompt_version: str
usage: TokenUsage
duration_ms: int
status: Literal["ok", "fallback", "failed"] # 对齐 runtime §2.2
@dataclass
class StructuredResult:
data: dict
raw_text: str
parse_attempts: int
model: str
prompt_version: str
usage: TokenUsage
duration_ms: int
status: Literal["ok", "fallback", "parse_error", "failed"]
# 补丁 1status 字段(覆盖 runtime 语义:ok / parse_error / failed
# fallback 表示主模型失败后经备用模型成功)
error: str | None = None # parse_error/failed 时的可读原因(诊断用)
@dataclass
class Prompt:
name: str
version: str
template: str
```
### 3.3 LLM 客户端(`client.py`
```python
class LLMClient(Protocol):
"""HTTP 适配器(可注入)。"""
def chat(self, *, model: str, messages: list[ChatMessage],
temperature: float, max_tokens: int) -> tuple[str, TokenUsage]: ...
class HttpLLMClient:
"""基于 httpx 的 OpenAI Chat Completions 兼容实现;base_url/api_key 从配置注入。
接受可选 transporthttpx.MockTransport)便于离线测试。"""
```
- 端点:`POST {base_url}/v1/chat/completions``Authorization: Bearer {api_key}`
- `base_url` / `api_key` 来自 `LLM_BASE_URL` / `DEEPSEEK_API_KEY`config-design §2
- 超时:`timeout_sec`(默认 60)→ **LLMTimeoutError**
- 网络/HTTP 非 2xx → **LLMNetworkError**(可重试)
- 5xx 重试(指数退避 `retry_backoff=[1,3,7]` 秒)→ 重试耗尽 → 主模型失败
### 3.4 重试 / 超时 / 降级(对齐 runtime §2.5
```
主模型超时或 5xx → 指数退避重试(1s/3s/7s)→ 仍失败
→ 降级备用模型(fallback,重试同上)→ 备用也失败 → status="failed"
结构化输出:JSON 解析失败 → 带错误信息重试(structured_output.max_parse_retry=2
→ 重试仍失败 → status="parse_error"(不抛异常),附原始文本
```
### 3.5 Token 管理(对齐 runtime §2.6 / config §4
- 估算:先 `tiktoken`(若已安装),否则 `approximate` 近似(4 字符≈1 token),实现 `TokenEstimator`tiktoken/approximate 双后端,配置驱动 `token_estimation`
- 超限策略由**注入的裁剪回调** `truncate_cb(prompt_text, vars) -> vars` 承担(按 config `truncation_policy.priority`),引擎只负责检测与触发,不内置裁剪实现(便于单测与职责分离)
### 3.6 Prompt 注册表(对齐 runtime §2.7
```python
class PromptRegistry:
def register(self, name: str, version: str, template: str) -> None: ...
def get(self, name: str, version: str | None = None, variables: dict | None = None) -> str: ...
def list_versions(self, name: str) -> list[str]: ...
```
- v1 实现:代码内 `register` + 目录加载(`prompts/{name}/{version}.txt`,配置 `prompts_dir`
- 渲染:Jinja2 语法(`{{ var }}`);`get(..., variables)` 返回渲染后文本;记录 `prompt_version` 供可追溯
### 3.7 引擎(`__init__.py`
```python
class InferenceEngine:
def __init__(self, *, client: LLMClient, models: InferenceModels,
registry: PromptRegistry | None = None, config: InferenceConfig | None = None): ...
def chat(self, *, session_id: str, prompt: Prompt | str, variables: dict,
model: str | None = None, temperature: float = 0.2,
max_tokens: int = 4096) -> ChatResult: ...
def chat_structured(self, *, session_id: str, prompt: Prompt | str, variables: dict,
schema: JSONSchema, retry_count: int = 2) -> StructuredResult: ...
```
### 3.8 异常模型(`exceptions.py`)——补丁 2
```python
class LLMError(Exception): ... # 基类,api-design §7 映射基底
class LLMNetworkError(LLMError): ... # 网络失败/5xx 重试耗尽(可重试语义)
class LLMTimeoutError(LLMError): ... # 超时 → api-error LLM_TIMEOUT(502)
class LLMNotConfiguredError(LLMError): ... # Key/模型缺失 → api-error LLM_NOT_CONFIGURED(503)
class LLMResponseError(LLMError): ... # 结构损坏(JSON 解析失败)
```
**api-design §7 错误码表修订(补丁 2)**
| 错误码 | HTTP | 含义 | 用户选项 | 来源 |
|--------|------|------|---------|------|
| `LLM_TIMEOUT` | 502 | LLM 调用超时 | retry / skip / abort | exceptions.LLMTimeoutError |
| `LLM_PARSE_ERROR` | 502 | 结构化输出解析失败 | retry | exceptions.LLMResponseError |
| `LLM_NOT_CONFIGURED` | 503 | 主/备用模型 Key 缺失 | 配置 Key | exceptions.LLMNotConfiguredError ← **新增行** |
### 3.9 既有文档修订汇总(补丁落实)
| 文档 | 修订 |
|------|------|
| `agent-runtime-design.md` §2.2 | `StructuredResult` 增加 `status` 字段(补丁 1 |
| `api-design.md` §7 | 错误码表新增 `LLM_NOT_CONFIGURED` 行(补丁 2 |
| `config-design.md` §4 | `token_estimation` 默认值由 `tiktoken` 改为 `approximate`(恢复 config.py 现有默认) |
> 注意:`config.py` 的 `LlmCallsConfig.token_estimation` 默认值目前是 `"tiktoken"`config.py:61)。实现
> 时以 **approximate 为内置实现**(零依赖、可离线),`tiktoken` 作为可选后端:若已安装则优先使用、
> 未安装则回落 approximate。因此 **不改 config 默认值**,保持 `"tiktoken"`(语义=优先 tiktoken
> 缺失自动降级);上述 config-design 修订为**注明该降级行为**,不改变配置默认值。
### 3.10 测试策略
- **单元测试(注入 FakeAdapter / FakeClient,零网络)**
- 主模型成功 / 主模型失败→重试→备用模型成功(status="fallback"/ 双模型全失败(status="failed"
- 超时→LLMTimeoutError;网络 5xx 重试耗尽→LLMNetworkError;未配置 Key→LLMNotConfiguredError
- `chat_structured`:一次成功 / 解析失败重试成功 / 重试耗尽→parse_error+raw_text
- Token 超限→裁剪回调被触发且返回新 variables
- Prompt 渲染 / 版本回退 / 注册表 list_versions
- **在线用例(可选,`@pytest.mark.llm_online`,默认不跑)**:真实 `DEEPSEEK_API_KEY` 已配时执行
- 全量回归 `python -m pytest` 全绿(含既有 53 用例)
## 4. 验收标准
1. `InferenceEngine.chat/chat_structured` 接口与 runtime §2.2 一致(含补丁 1 字段)
2. 重试/降级/超时/未配置/解析失败/Token 裁剪 —— 全路径注入测试通过(零网络)
3. 结构化失败返回 `parse_error` 且附 raw_text,不抛异常
4. api-design §7 表含 `LLM_NOT_CONFIGURED`(补丁 2);文档补丁到位
5. `pytest -m pytest -v` 全绿(新增用例 beyond 53
## 5. 非目标(显式延后)
- LLM 调用缓存、成本/限流监控(runtime v2 §9
- function callingImageAnalyzer 的 tool call 由 v2 预留)
- 在线真实调用回归(仅可选标记,不在默认 CI)
## 6. 涉及文件
- 新建:`src/genesis/inference/{__init__,errors,client,token,prompt_registry,types,exceptions}.py`
- 修改:`pyproject.toml`(新增 `httpx` 依赖;`jinja2` 用于 PromptRegistry 渲染)
- 修改:`docs/agent-runtime-design.md`(§2.2 补丁 1)、`docs/api-design.md`(§7 补丁 2)、`docs/config-design.md`(§4 注明 tiktoken 双语义)
- 新建:`tests/test_inference_types.py``tests/test_inference_errors.py``tests/test_inference_token.py``tests/test_inference_prompt_registry.py``tests/test_inference_client.py``tests/test_inference_engine.py`(平铺于 tests/,与既有 excel_helpers.py 惯例一致);`tests/inference_helpers.py`FakeClient / FakeTransport 构造)
## 7. 版本与状态
| 版本 | 日期 | 说明 |
|------|------|------|
| v1.0 | 2026-08-09 | 批准版(含补丁 1/2|
后续:由 `writing-plans` 生成实施计划(task A-G→5 个实操包),进入 `/implement` backlog。