docs: 同步 core/protocols.py 分层、四层治理栈、遥测新字段

ARCHITECTURE.md §2.3/§2.4/§3.1/§4/§5 + CLAUDE.md §4.8/§4.9/§5 + .env.example
对应设计 research-wiki/designs/2026-07-06-core-agent-adapters-llm-design.md §8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 22:30:16 -04:00
parent bc78138d8f
commit 6b7c28ea1e
3 changed files with 72 additions and 44 deletions
+5 -1
View File
@@ -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
+11 -8
View File
@@ -243,7 +243,7 @@ MODE=mock N_SAMPLES=10 bash scripts/<experiment>.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/<experiment>.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/<experiment>.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 # 跨模块共享类型
+56 -35
View File
@@ -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_tokentoken 间隔)、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
```
---