diff --git a/.env.example b/.env.example index 4105701..bf55e06 100644 --- a/.env.example +++ b/.env.example @@ -34,7 +34,7 @@ MONKEY_OCR_URLS=http://10.77.0.20:7866,http://10.77.0.20:7867 EMBED_API_KEY= EMBED_API_URL= -# ── Redis(响应缓存 + ARQ 任务队列)── +# ── Redis(响应缓存)── REDIS_URL=redis://localhost:6379/0 # ── LLM 韧性参数 ── @@ -43,3 +43,7 @@ LLM_MAX_RETRIES=3 LLM_RETRY_BASE_DELAY=2.0 LLM_CIRCUIT_BREAKER_THRESHOLD=5 LLM_CIRCUIT_BREAKER_COOLDOWN=60 +LLM_TTFT_TIMEOUT=30 +LLM_INTER_TOKEN_TIMEOUT=15 +LLM_RETRY_MAX_DELAY=30.0 +REDIS_CACHE_TTL=86400 diff --git a/CLAUDE.md b/CLAUDE.md index e524f4e..0bfae73 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -243,7 +243,7 @@ MODE=mock N_SAMPLES=10 bash scripts/.sh # smoke test ### 4.8 Agent 遥测 -每次 LLM/VLM 调用必须经过 `TelemetryRecorder` Protocol(`core/evolution/protocols.py`)记录。完整规范见 `research-wiki/ARCHITECTURE.md §4`。 +每次 LLM/VLM 调用必须经过 `TelemetryRecorder` Protocol(`core/protocols.py`)记录。完整规范见 `research-wiki/ARCHITECTURE.md §4`。 | 必录字段 | 类型 | 说明 | |----------|------|------| @@ -254,8 +254,11 @@ MODE=mock N_SAMPLES=10 bash scripts/.sh # smoke test | `provider` | str | API 端点标识 | | `messages` | str (JSON) | 原始输入 | | `response` | str | 原始输出 | +| `thinking` | str | thinking/reasoning 内容 | | `prompt_tokens` / `completion_tokens` | int | token 用量 | | `latency_ms` | int | 延迟毫秒 | +| `ttft_ms` | float? | 首 token 延迟(流式测量) | +| `max_inter_token_ms` | float? | 最大 token 间隔(流式测量) | | `cache_hit` | bool | 是否命中 Redis 缓存 | | `error` | str? | 异常信息(正常为 null) | @@ -263,17 +266,16 @@ MODE=mock N_SAMPLES=10 bash scripts/.sh # smoke test ### 4.9 LLM 韧性 -所有 LLM/VLM 调用必须经过 `GovernedLLMClient`(`adapters/llm.py`)治理,禁止裸调 OpenAI SDK。治理栈五层(详见 `research-wiki/ARCHITECTURE.md §5`): +所有 LLM/VLM 调用必须经过 `GovernedLLMClient`(`adapters/llm.py`)治理,禁止裸调 OpenAI SDK。治理栈四层(详见 `research-wiki/ARCHITECTURE.md §5`): | 层 | 机制 | 说明 | |---|------|------| -| 1 | 硬超时 | `asyncio.wait_for(call, timeout=config.llm_timeout)` | -| 2 | 指数退避重试 | `max_retries`、`base_delay`、`max_delay`(可配置) | -| 3 | 熔断器 | 连续 N 失败 → 短路 M 秒 → 探针恢复 | -| 4 | Redis 响应缓存 | content-addressed:`hash(model + messages)` → response | -| 5 | ARQ 任务队列 | 长时间推理任务异步执行 | +| 1 | 熔断器 | 连续 N 失败 → 短路 M 秒 → 探针恢复(`adapters/breaker.py`) | +| 2 | Redis 响应缓存 | content-addressed:`hash(model + messages)` → response | +| 3 | 流式三层看门狗 | TTFT / inter_token / total 超时保护(`adapters/streaming.py`) | +| 4 | 指数退避重试 | `max_retries`、`base_delay`、`max_delay`(可配置) | -> 熔断参数(`LLM_CIRCUIT_BREAKER_THRESHOLD`、`LLM_CIRCUIT_BREAKER_COOLDOWN`)和超时(`LLM_TIMEOUT`)通过 `.env` 工程配置管理。 +> 熔断参数(`LLM_CIRCUIT_BREAKER_THRESHOLD`、`LLM_CIRCUIT_BREAKER_COOLDOWN`)和超时(`LLM_TIMEOUT`、`LLM_TTFT_TIMEOUT`、`LLM_INTER_TOKEN_TIMEOUT`)通过 `.env` 工程配置管理。 ## 5. 项目结构规范 @@ -289,6 +291,7 @@ project_root/ ├── .claude/ # Claude 配置(skills, hooks, tools) │ ├── core/ # 可提取内核(不依赖 app/、adapters/) +│ ├── protocols.py # 共享端口(LLMProvider, VLMProvider, TelemetryRecorder) │ ├── agent/ # AgentLoop 引擎(loop, types, protocols) │ ├── evolution/ # 诊断+进化引擎(diagnose, evolve, gate) │ └── types.py # 跨模块共享类型 diff --git a/research-wiki/ARCHITECTURE.md b/research-wiki/ARCHITECTURE.md index 4a67ac4..3d85e7c 100644 --- a/research-wiki/ARCHITECTURE.md +++ b/research-wiki/ARCHITECTURE.md @@ -103,17 +103,18 @@ flowchart TD ```text project_root/ ├── core/ # 可提取内核(不依赖 app/、adapters/) +│ ├── protocols.py # 共享端口:LLMProvider, VLMProvider, TelemetryRecorder │ ├── agent/ # 【可提取包】AgentLoop 引擎 │ │ ├── loop.py # Thinking+JSON 推理循环,pluggy hook │ │ ├── types.py # Step, LoopResult -│ │ └── protocols.py # LLMProvider, ToolDispatcher Protocol +│ │ └── protocols.py # Agent 专属:ToolDispatcher, AgentLoopSpec │ ├── evolution/ # 【可提取包】诊断+进化引擎 │ │ ├── diagnose.py # 两阶段诊断管线 │ │ ├── evolve.py # patch/rewrite 进化 │ │ ├── gate.py # CE-Gate e-process │ │ ├── validate.py # 块顺序验证 │ │ ├── types.py # DiagnosisResult, EvolutionRecord, ... -│ │ └── protocols.py # SkillStore, RunLog, TelemetryRecorder Protocol +│ │ └── protocols.py # Evolution 专属:SkillStore, PromptStore, RunLog │ └── types.py # 跨模块共享类型 │ ├── app/ # 应用层(组合 core + adapters,领域特化) @@ -148,7 +149,9 @@ project_root/ │ └── ports.py # 应用层特有端口 │ ├── adapters/ # 外部实现层 -│ ├── llm.py # GovernedLLMClient(遥测+熔断+Redis缓存) +│ ├── llm.py # GovernedLLMClient(四层治理栈) +│ ├── streaming.py # 流式三层看门狗(TTFT/inter_token/total) +│ ├── breaker.py # CircuitBreaker(内存级熔断器) │ ├── vlm.py # VLM 客户端 │ ├── embedding.py # Embedding 服务实现 │ ├── redis_cache.py # Redis 响应缓存 @@ -172,7 +175,7 @@ project_root/ | 层 | 可依赖 | 禁止依赖 | |---|--------|---------| -| `core/` | 标准库、typing、pluggy | `app/`、`adapters/`、任何框架 | +| `core/` | 标准库、typing、pluggy、json_repair | `app/`、`adapters/`、任何框架 | | `app/` | `core/`、标准库 | `adapters/`(只通过 Protocol) | | `adapters/` | `core/`、`app/ports.py`、第三方库 | `app/` 内部模块 | @@ -188,15 +191,28 @@ project_root/ ### 3.1 核心端口(`core/` 内,可提取) -| Protocol | 所在文件 | 关键方法 | 职责 | -|----------|---------|---------|------| -| `LLMProvider` | `core/agent/protocols.py` | `chat()`, `chat_async()` | LLM 文本调用 | -| `VLMProvider` | `core/agent/protocols.py` | `chat_with_images()`, `chat_with_images_async()` | VLM 图文调用 | -| `ToolDispatcher` | `core/agent/protocols.py` | `dispatch(tool_name, args, context)` | Agent 工具调度 | -| `SkillStore` | `core/evolution/protocols.py` | `read_skill()`, `write_skill()`, `list_versions()` | 版本化技能存储 | -| `PromptStore` | `core/evolution/protocols.py` | `read_prompt()`, `write_prompt()` | 版本化提示词存储 | -| `RunLog` | `core/evolution/protocols.py` | `insert()`, `query()` | 实验日志 | -| `TelemetryRecorder` | `core/evolution/protocols.py` | `record_llm_call()` | Agent 遥测 | +**共享端口(`core/protocols.py`,跨子包):** + +| Protocol | 关键方法 | 职责 | +|----------|---------|------| +| `LLMProvider` | `chat()` | LLM 文本调用(全异步) | +| `VLMProvider` | `chat_with_images()` | VLM 图文调用(全异步) | +| `TelemetryRecorder` | `record_llm_call()` | LLM 调用遥测 | + +**Agent 专属端口(`core/agent/protocols.py`):** + +| Protocol | 关键方法 | 职责 | +|----------|---------|------| +| `ToolDispatcher` | `dispatch(tool_name, args, context)` | Agent 工具调度 | +| `AgentLoopSpec` | `before_step/after_tool/after_step/on_finish` | pluggy 生命周期 hook | + +**Evolution 专属端口(`core/evolution/protocols.py`):** + +| Protocol | 关键方法 | 职责 | +|----------|---------|------| +| `SkillStore` | `read_skill()`, `write_skill()`, `list_versions()` | 版本化技能存储 | +| `PromptStore` | `read_prompt()`, `write_prompt()` | 版本化提示词存储 | +| `RunLog` | `insert()`, `query()` | 实验日志 | ### 3.2 应用层端口(`app/ports.py`) @@ -239,9 +255,12 @@ project_root/ | `provider` | str | API 端点标识 | | `messages` | str (JSON) | 原始输入 | | `response` | str | 原始输出 | +| `thinking` | str | thinking/reasoning 内容 | | `prompt_tokens` | int | 输入 token 数 | | `completion_tokens` | int | 输出 token 数 | | `latency_ms` | int | 延迟毫秒 | +| `ttft_ms` | float? | 首 token 延迟(流式测量) | +| `max_inter_token_ms` | float? | 最大 token 间隔(流式测量) | | `cache_hit` | bool | 是否命中 Redis 缓存 | | `error` | str? | 异常信息(正常为 null) | @@ -251,37 +270,39 @@ project_root/ ## §5 LLM 调用韧性 -所有 LLM/VLM 调用必须经过 `GovernedLLMClient`(`adapters/llm.py`)治理,禁止裸调 OpenAI SDK。治理栈包含五层: +所有 LLM/VLM 调用必须经过 `GovernedLLMClient`(`adapters/llm.py`)治理,禁止裸调 OpenAI SDK。治理栈包含四层: | 层 | 机制 | 说明 | |---|------|------| -| 1 | 硬超时 | `asyncio.wait_for(call, timeout=config.llm_timeout)` | -| 2 | 指数退避重试 | `max_retries`、`base_delay`、`max_delay`(可配置) | -| 3 | 熔断器 | 连续 N 失败 → 短路 M 秒 → 探针恢复 | -| 4 | Redis 响应缓存 | content-addressed:`hash(model + messages)` → response | -| 5 | ARQ 任务队列 | 长时间推理任务异步执行 | +| 1 | 熔断器 | 连续 N 失败 → 短路 M 秒 → 探针恢复(`adapters/breaker.py`) | +| 2 | Redis 响应缓存 | content-addressed:`hash(model + messages)` → response | +| 3 | 流式三层看门狗 | TTFT / inter_token / total 超时保护(`adapters/streaming.py`) | +| 4 | 指数退避重试 | `max_retries`、`base_delay`、`max_delay`(可配置) | -熔断参数(`LLM_CIRCUIT_BREAKER_THRESHOLD`、`LLM_CIRCUIT_BREAKER_COOLDOWN`)和超时(`LLM_TIMEOUT`)通过 `.env` 工程配置管理。 +**流式三层看门狗**:GovernedLLMClient 内部始终使用 `stream=True` 消费 SSE,通过 `stream_with_liveness_timeouts()` 实现三层超时保护——TTFT(首 token,含 thinking token)、inter_token(token 间隔)、total(总时长)。thinking token 刷新看门狗但不计入 content,确保长时间思考不会被误判为连接挂死。 + +熔断参数(`LLM_CIRCUIT_BREAKER_THRESHOLD`、`LLM_CIRCUIT_BREAKER_COOLDOWN`)、超时(`LLM_TIMEOUT`、`LLM_TTFT_TIMEOUT`、`LLM_INTER_TOKEN_TIMEOUT`)通过 `.env` 工程配置管理。 **治理流程伪代码**: ``` -call(messages) → - if breaker.is_open: raise CircuitOpen - key = hash(model, messages) - if redis.exists(key): return cached # 层4: 缓存 - try: - resp = await wait_for(api(messages), # 层1: 硬超时 - timeout) +chat(messages) → + if breaker.is_open: raise CircuitOpen # 层1: 熔断 + call_id = uuid4() + if cache.get(model, messages): # 层2: 缓存 + telemetry.record(..., cache_hit=True) + return cached + retry loop: # 层4: 重试 + stream = httpx.stream(POST, stream=True) + guarded = stream_with_liveness_timeouts( # 层3: 三层看门狗 + sse_deltas, ttft, inter_token, total) + content, thinking, ttft_ms = consume(guarded) breaker.record_success() - redis.set(key, resp) - telemetry.record(...) # 遥测 - return resp - except Transient: - retry with backoff # 层2: 退避重试 - except: - breaker.record_failure() # 层3: 熔断 - raise + cache.set(model, messages, response) + telemetry.record(...) + return response + except transient → backoff + breaker.record_failure() + except 401/403 → breaker.force_open(); raise ``` ---