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

11 KiB
Raw Blame History

里程碑 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
    • 补丁 1StructuredResult 增加 status: Literal["ok","parse_error","failed"] 字段(基线 runtime §2.2 无此字段)
    • 补丁 2:错误码 LLM_NOT_CONFIGURED503)补入 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 客户端选择 httpxpyproject 新增依赖),采用异步 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

@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

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/completionsAuthorization: Bearer {api_key}
  • base_url / api_key 来自 LLM_BASE_URL / DEEPSEEK_API_KEYconfig-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),实现 TokenEstimatortiktoken/approximate 双后端,配置驱动 token_estimation
  • 超限策略由注入的裁剪回调 truncate_cb(prompt_text, vars) -> vars 承担(按 config truncation_policy.priority),引擎只负责检测与触发,不内置裁剪实现(便于单测与职责分离)

3.6 Prompt 注册表(对齐 runtime §2.7

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

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

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.pyLlmCallsConfig.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.pytests/test_inference_errors.pytests/test_inference_token.pytests/test_inference_prompt_registry.pytests/test_inference_client.pytests/test_inference_engine.py(平铺于 tests/,与既有 excel_helpers.py 惯例一致);tests/inference_helpers.pyFakeClient / FakeTransport 构造)

7. 版本与状态

版本 日期 说明
v1.0 2026-08-09 批准版(含补丁 1/2

后续:由 writing-plans 生成实施计划(task A-G→5 个实操包),进入 /implement backlog。