From f541c4047a37a75506d97261ceb83fcec9d72113 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Mon, 6 Jul 2026 22:06:48 -0400 Subject: [PATCH] =?UTF-8?q?design:=20core/agent/=20+=20adapters/llm=20?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 全异步 AgentLoop + 四层治理栈(超时→重试→熔断→缓存)+ 流式三层看门狗。 LLMProvider/VLMProvider/TelemetryRecorder 上提到 core/protocols.py 作为共享端口。 经 Codex 独立审查,修复 7 项发现(含缓存遥测、inter_token 指标、json_repair 依赖等)。 Co-Authored-By: Claude Opus 4.6 (1M context) --- ...26-07-06-core-agent-adapters-llm-design.md | 428 ++++++++++++++++++ .../designs/core-agent-adapters-llm.md | 9 + research-wiki/graph/edges.json | 5 + research-wiki/index.md | 6 +- research-wiki/log.md | 2 + 5 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 research-wiki/designs/2026-07-06-core-agent-adapters-llm-design.md create mode 100644 research-wiki/designs/core-agent-adapters-llm.md diff --git a/research-wiki/designs/2026-07-06-core-agent-adapters-llm-design.md b/research-wiki/designs/2026-07-06-core-agent-adapters-llm-design.md new file mode 100644 index 0000000..00d2ce5 --- /dev/null +++ b/research-wiki/designs/2026-07-06-core-agent-adapters-llm-design.md @@ -0,0 +1,428 @@ +# core/agent/ + adapters/llm 基础设施设计 + +**日期** 2026-07-06 · **状态** 已批准 · **涉及算法保真** #11 Agent Loop + +--- + +## §1 设计决策总结 + +| 决策点 | 结论 | 理由 | +|--------|------|------| +| 同步 vs 异步 | **全异步** | 流式 SSE + asyncio.timeout 看门狗 + 未来 ARQ 扩展 | +| LLMProvider 返回值 | **完整 LLMResponse**(adapters 内部流式消费) | core 不需逐 token 处理;TTFT 是基础设施指标属 adapters 层 | +| thinking 剥离层 | **adapters 层统一剥离** | core 不感知 provider 差异;新增 provider 只改 adapters | +| 遥测写入 | **两层写入** | adapters: 原始 LLM 指标 → telemetry.db(GovernedLLMClient 自动);core: agent 行为轨迹通过 pluggy hook 由外部插件(如 HarnessLog 的 TracePlugin)写入 | +| LLMProvider 归属 | **core/protocols.py**(共享端口) | agent、建树、诊断、进化都需要;避免 core 子包互相依赖 | +| ARQ 任务队列 | **砍掉(YAGNI)** | CLI 驱动研究工具,asyncio 原生并发够用;跨进程场景靠重试退避自然降级 | +| adapters 组织 | **组合式** | 拆成独立可测组件;GovernedLLMClient 组合它们 | + +--- + +## §2 文件清单与依赖方向 + +### 2.1 新建文件 + +| 文件 | 职责 | +|------|------| +| `core/protocols.py` | 共享端口:LLMProvider, VLMProvider, TelemetryRecorder | +| `core/agent/protocols.py` | Agent 专属端口:ToolDispatcher, AgentLoopSpec | +| `core/agent/types.py` | Step, LoopResult | +| `core/agent/loop.py` | AgentLoop 引擎 | +| `adapters/llm.py` | GovernedLLMClient(实现 LLMProvider,组合治理栈) | +| `adapters/streaming.py` | stream_with_liveness_timeouts() 三层看门狗 | +| `adapters/breaker.py` | CircuitBreaker(内存级) | +| `adapters/redis_cache.py` | RedisResponseCache(content-addressed) | +| `adapters/telemetry.py` | SQLiteTelemetryRecorder(实现 TelemetryRecorder) | + +### 2.2 修改文件 + +| 文件 | 变更内容 | +|------|---------| +| `core/types.py` | 新增 LLMResponse dataclass | +| `ARCHITECTURE.md` | §3.1 接缝清单重构(共享/专属分类);§4 遥测新增 thinking + ttft_ms 字段;§5 治理栈从五层改为四层 | +| `CLAUDE.md` | 同步 §4.8 遥测字段、§4.9 治理栈层数 | + +### 2.3 依赖方向 + +```mermaid +flowchart TB + subgraph core + CP["core/protocols.py\nLLMProvider, VLMProvider\nTelemetryRecorder"] + CT["core/types.py\nLLMResponse"] + AP["core/agent/protocols.py\nToolDispatcher, AgentLoopSpec"] + AT["core/agent/types.py\nStep, LoopResult"] + AL["core/agent/loop.py\nAgentLoop"] + end + + subgraph adapters + GL["adapters/llm.py\nGovernedLLMClient"] + ST["adapters/streaming.py\n三层看门狗"] + BR["adapters/breaker.py\nCircuitBreaker"] + RC["adapters/redis_cache.py\nRedisResponseCache"] + TL["adapters/telemetry.py\nSQLiteTelemetryRecorder"] + end + + AL --> CP & AP & AT & CT + AP --> AT + GL -->|实现| CP + GL --> ST & BR & RC & TL & CT + TL -->|实现| CP +``` + +--- + +## §3 core/protocols.py — 共享端口 + +三个 `@runtime_checkable` Protocol。 + +### LLMProvider + +```python +class LLMProvider(Protocol): + async def chat( + self, + messages: list[dict[str, Any]], + *, + session_id: str | None = None, + parent_call_id: str | None = None, + ) -> LLMResponse: ... +``` + +- `session_id`:epoch/step/question 关联,遥测链路追踪 +- `parent_call_id`:agent step → LLM call 父子关系 +- 无 model/temperature——实例配置,构造时确定 + +### VLMProvider + +```python +class VLMProvider(Protocol): + async def chat_with_images( + self, + messages: list[dict[str, Any]], + images: list[str | Path], + *, + session_id: str | None = None, + parent_call_id: str | None = None, + ) -> LLMResponse: ... +``` + +与 LLMProvider 分离,不继承——职责不同,非所有 LLM 支持图片。返回同一 LLMResponse 类型。 + +### TelemetryRecorder + +```python +class TelemetryRecorder(Protocol): + async def record_llm_call( + self, + *, + call_id: str, + parent_call_id: str | None, + session_id: str | None, + model_name: str, + provider: str, + messages: str, + response: str, + thinking: str, + prompt_tokens: int, + completion_tokens: int, + latency_ms: int, + ttft_ms: float | None, + max_inter_token_ms: float | None, + cache_hit: bool, + error: str | None, + ) -> None: ... +``` + +比 ARCHITECTURE.md §4 原方案新增 `thinking`、`ttft_ms`、`max_inter_token_ms` 字段。全部 keyword-only。 + +--- + +## §4 core/types.py — LLMResponse + +```python +@dataclass(frozen=True) +class LLMResponse: + content: str # 纯输出(thinking 已剥离) + thinking: str # thinking/reasoning 内容(无则空串) + model: str # 实际模型名 + provider: str # API 端点标识 + prompt_tokens: int + completion_tokens: int + latency_ms: int # 总延迟 + ttft_ms: float | None # 首 token 延迟(缓存命中时 None) + max_inter_token_ms: float | None # 最大 token 间隔(缓存命中时 None) + cache_hit: bool + call_id: str # UUID,调用唯一标识 +``` + +- `frozen=True`:响应是不可变事实 +- `call_id` 随响应带出,core 层用于关联 agent step → LLM call +- VLMProvider 返回同一类型,不单独定义 VLMResponse + +--- + +## §5 core/agent/ — AgentLoop 可提取内核 + +### 5.1 core/agent/types.py + +```python +@dataclass +class Step: + thought: str # thinking/reasoning 内容 + reflect: dict[str, Any] # 结构化反思 + plan: dict[str, Any] # 结构化计划 + tool_call: dict[str, Any] # {"tool": name, "args": {...}} + tool_output: str # 工具执行结果 + raw_content: str # LLM 原始 JSON 输出 + call_id: str # 关联 LLMResponse.call_id + +@dataclass +class LoopResult: + result: dict[str, Any] | None = None + steps: list[Step] = field(default_factory=list) + steps_used: int = 0 + token_usage: dict[str, int] = field( + default_factory=lambda: {"prompt_tokens": 0, "completion_tokens": 0} + ) + stop_reason: str = "finished" + # "finished" | "error" | "parse_error" | "budget_exceeded" +``` + +vs TRM4:Step 新增 `call_id`,其余结构保持一致(算法保真 #11)。 + +### 5.2 core/agent/protocols.py + +```python +class ToolDispatcher(Protocol): + async def dispatch( + self, tool_name: str, args: dict[str, Any], *, context: dict[str, Any] + ) -> str: ... +``` + +vs TRM4:从 `Callable[[str, dict], str]` 升级为 Protocol;新增 `context`;async 化。无效工具名抛 `ValueError`。 + +**AgentLoopSpec** — pluggy hookspec,四个 async 生命周期 hook: + +| hook | 签名 | 说明 | +|------|------|------| +| `before_step` | `(iteration, messages) -> None` | LLM 调用前 | +| `after_tool` | `(iteration, step) -> str \| None` | 工具执行后;返回非 None 注入反馈 | +| `after_step` | `(iteration, messages) -> None` | 整轮结束后 | +| `on_finish` | `(result: LoopResult) -> None` | 循环终止时 | + +**Agent 行为轨迹记录机制**:AgentLoop 本身不写日志,行为轨迹(step/thought/tool_call)通过 pluggy hook 暴露给外部插件。`app/harness/` 的 `TracePlugin` 注册为 pluggy 插件,在 `after_step`/`on_finish` 中写入 `HarnessLog`。这实现了"两层遥测"中 core 层的职责——机制在 core,策略在 app。 + +### 5.3 core/agent/loop.py — AgentLoop + +```python +class AgentLoop: + def __init__( + self, + llm: LLMProvider, + max_steps: int, + max_retries: int = 3, + ) -> None: ... + + async def run( + self, + system_prompt: str, + user_prompt: str, + tool_dispatcher: ToolDispatcher, + plugins: list[Any] | None = None, + *, + session_id: str | None = None, + ) -> LoopResult: ... +``` + +核心循环保真 TRM4 算法 #11: + +``` +messages = [system, user] +while steps_used < max_steps: + await hooks.before_step(iteration, messages) + response = await llm.chat(messages, session_id=session_id, + parent_call_id=last_call_id) + 累加 token_usage + parsed = _parse_response(response) # json_repair 兜底 + if 连续 parse 失败 > max_retries: break(parse_error) + step = Step(..., call_id=response.call_id) + tool_output = await tool_dispatcher.dispatch(tool, args, context=...) + feedback = await hooks.after_tool(iteration, step) + 组装 messages(工具结果 + 可选 feedback) + await hooks.after_step(iteration, messages) + if tool == "submit_answer": break(finished) +await hooks.on_finish(result) +``` + +保真点:`json_repair.repair_json()`、解析失败纠正 prompt、`submit_answer` 终止、pluggy hook 生命周期。 + +**依赖说明**:`core/` 允许依赖 `stdlib`、`typing`、`pluggy`、`json_repair`。`json_repair` 是 AgentLoop 算法保真 #11 的必要依赖(TRM4 已使用),作为 core 允许的第三方库显式列入。 + +--- + +## §6 adapters/ — 四层治理栈 + +### 6.1 adapters/streaming.py — 三层看门狗 + +```python +class StreamLivenessTimeout(Exception): + kind: str # "ttft" | "inter_token" | "total" + elapsed_s: float + first_token_seen: bool + +async def stream_with_liveness_timeouts( + source: AsyncIterator[tuple[bool, str]], + *, ttft_s: float, inter_token_s: float, total_s: float, +) -> AsyncIterator[tuple[bool, str]]: ... +``` + +- 入参 `(is_content, text)`——thinking token(`is_content=False`)刷新看门狗但不计入 content +- `min(layer_budget, remaining_total)` 合并三层为单循环 +- 只 wrap `__anext__`,不 wrap `yield`(防取消泄漏) +- `asyncio.timeout()` + `cm.expired()` 区分本层 vs 上游超时 + +参考:CHSAnalyzer2 `app/providers/streaming.py` + +### 6.2 adapters/breaker.py — 熔断器 + +```python +class CircuitBreaker: + def __init__(self, fail_threshold: int, cooldown_s: float) -> None: ... + def is_open(self, now: float) -> bool: ... + def record_failure(self, now: float) -> None: ... + def record_success(self) -> None: ... + def force_open(self, now: float) -> None: ... +``` + +- 注入 `now`,纯确定性可测试 +- 内存级单实例,半开探针自动恢复 +- `force_open` 用于 401/403 直接熔断 + +参考:CHSAnalyzer2 `app/providers/breaker.py` + +### 6.3 adapters/redis_cache.py — 响应缓存 + +```python +class RedisResponseCache: + def __init__(self, redis: redis.asyncio.Redis, ttl_s: int) -> None: ... + async def get(self, model: str, messages: list[dict]) -> LLMResponse | None: ... + async def set(self, model: str, messages: list[dict], response: LLMResponse) -> None: ... +``` + +- content-addressed:`key = sha256(model + json_canonical(messages))` +- Redis 不可用时静默降级(`get` 返回 None,`set` 吞异常 + 日志告警) +- TTL 通过 `.env` 工程配置管理 + +### 6.4 adapters/telemetry.py — 遥测记录 + +```python +class SQLiteTelemetryRecorder: + def __init__(self, db_path: Path) -> None: ... + async def record_llm_call(self, *, ...) -> None: ... +``` + +- 实现 `TelemetryRecorder` Protocol +- SQLite 写入通过 `asyncio.to_thread()` 桥接 +- `llm_calls` 表字段与 Protocol 一一对应 + +### 6.5 adapters/llm.py — GovernedLLMClient + +```python +class GovernedLLMClient: + def __init__( + self, *, + model: str, base_url: str, api_key: str, + provider: str, # "deepseek" | "qwen" | "unknown" + thinking: bool, + breaker: CircuitBreaker, + cache: RedisResponseCache | None, + telemetry: TelemetryRecorder, + timeout_s: float, + ttft_timeout_s: float | None, + inter_token_timeout_s: float | None, + max_retries: int, + retry_base_delay_s: float, + retry_max_delay_s: float, + ) -> None: ... + + async def chat(self, messages, *, session_id=None, parent_call_id=None) -> LLMResponse: ... +``` + +`chat()` 编排伪代码: + +``` +① breaker.is_open(now)? → raise CircuitOpenError +② call_id = uuid4() # 每次调用都生成,含缓存命中 +③ cached = cache.get(model, messages)? + telemetry.record_llm_call(call_id, ..., cache_hit=True) # 缓存命中也记遥测 + return cached._replace(call_id=call_id, cache_hit=True) +④ 重试循环: + started = monotonic() + stream = httpx_client.stream(POST /chat/completions, stream=True) + sse_deltas = _iter_sse_deltas(stream) + guarded = stream_with_liveness_timeouts(sse_deltas, ttft, inter_token, total) + content, thinking, ttft_ms, max_itoken_ms, tokens = _consume(guarded) + breaker.record_success() + except transient → backoff + breaker.record_failure() + except 401/403 → breaker.force_open(); raise +⑤ response = LLMResponse(content, thinking, ..., call_id, + ttft_ms, max_itoken_ms, cache_hit=False) +⑥ cache.set(model, messages, response) +⑦ telemetry.record_llm_call(call_id, ..., max_inter_token_ms, cache_hit=False) +⑧ return response +# 异常路径同样记遥测(error=str(e)) +``` + +provider 差异封装为私有方法: +- `_build_thinking_body()` — DeepSeek/Qwen thinking 参数差异 +- `_strip_thinking()` — Qwen `` 标签剥离 vs DeepSeek `reasoning_content` 字段提取 + +--- + +## §7 配置归属 + +| 参数 | 归属 | 载体 | +|------|------|------| +| `*_API_KEY`, `*_BASE_URL`, `*_MODEL` | 工程配置 | `.env` | +| `REDIS_URL` | 工程配置 | `.env` | +| `LLM_TIMEOUT` | 工程配置 | `.env` | +| `LLM_TTFT_TIMEOUT` | 工程配置 | `.env`(新增) | +| `LLM_INTER_TOKEN_TIMEOUT` | 工程配置 | `.env`(新增) | +| `LLM_MAX_RETRIES`, `LLM_RETRY_BASE_DELAY`, `LLM_RETRY_MAX_DELAY` | 工程配置 | `.env` | +| `LLM_CIRCUIT_BREAKER_THRESHOLD`, `LLM_CIRCUIT_BREAKER_COOLDOWN` | 工程配置 | `.env` | +| `REDIS_CACHE_TTL` | 工程配置 | `.env`(新增) | + +所有参数均为系统运行所需、少变——不会在实验中反复扫动,归工程配置。 + +--- + +## §8 ARCHITECTURE.md / CLAUDE.md / 配置文件同步变更 + +> **时机**:规范同步在实现开始前完成(作为实现计划的第一个任务),而非实现结束后补。 + +| 文档 | 章节 | 变更 | +|------|------|------| +| ARCHITECTURE.md §2.3 | 目录结构 | `core/` 下新增 `protocols.py`;`adapters/` 新增 `streaming.py`、`breaker.py` | +| ARCHITECTURE.md §2.4 | 依赖方向 | `core/` 允许依赖扩展为:标准库、typing、pluggy、json_repair | +| ARCHITECTURE.md §3.1 | 核心端口 | LLMProvider/VLMProvider/TelemetryRecorder 从 `core/agent/protocols.py` 和 `core/evolution/protocols.py` 上提到 `core/protocols.py`;ToolDispatcher/AgentLoopSpec 留在 `core/agent/protocols.py` | +| ARCHITECTURE.md §4 | 遥测规范 | 新增 `thinking`、`ttft_ms`、`max_inter_token_ms` 字段 | +| ARCHITECTURE.md §5 | 韧性治理 | 五层改四层(砍掉 ARQ,理由:CLI 研究工具无需投递式队列,asyncio 原生并发够用);新增流式三层看门狗说明 | +| CLAUDE.md §4.8 | Agent 遥测 | 同步新增字段(thinking、ttft_ms、max_inter_token_ms) | +| CLAUDE.md §4.9 | LLM 韧性 | 同步四层治理栈 + 三层看门狗;删除 ARQ 层说明 | +| CLAUDE.md §5 | 项目结构 | `core/` 下新增 `protocols.py` | +| `.env.example` | — | 新增 `LLM_TTFT_TIMEOUT`、`LLM_INTER_TOKEN_TIMEOUT`、`REDIS_CACHE_TTL` | + +--- + +## §9 被拒方案 + +| 被拒方案 | 理由 | +|---------|------| +| 同步 AgentLoop | 流式 SSE + asyncio.timeout 看门狗无法在同步模型中干净实现 | +| LLMProvider 返回流式迭代器 | core 不需逐 token 处理;TTFT 是基础设施指标不应泄漏到 core | +| 双接口(chat + chat_stream) | YAGNI——当前无实时展示需求 | +| core 层解析 thinking | 耦合 provider 特征,破坏可提取性 | +| 仅 adapters 层写遥测 | 漏掉 agent 行为级轨迹(step/thought/tool_call) | +| LLMProvider 留在 core/agent/protocols.py | evolution/tree 等非 agent 消费者被迫依赖 core/agent/,破坏子包独立性 | +| 保留 ARQ | CLI 研究工具不需要投递式任务队列;跨进程限流靠重试退避自然降级 | +| 单体 GovernedLLMClient | 500+ 行单文件,组件无法独立测试 | diff --git a/research-wiki/designs/core-agent-adapters-llm.md b/research-wiki/designs/core-agent-adapters-llm.md new file mode 100644 index 0000000..184cbac --- /dev/null +++ b/research-wiki/designs/core-agent-adapters-llm.md @@ -0,0 +1,9 @@ +--- +type: design +node_id: design:core-agent-adapters-llm +title: "core/agent/ + adapters/llm 基础设施设计" +date: 2026-07-07 +--- + +# core/agent/ + adapters/llm 基础设施设计 + diff --git a/research-wiki/graph/edges.json b/research-wiki/graph/edges.json index bac706c..9c852c1 100644 --- a/research-wiki/graph/edges.json +++ b/research-wiki/graph/edges.json @@ -5,6 +5,11 @@ "id": "plan:infrastructure-setup", "label": "项目基础设施初始化计划", "type": "plan" + }, + { + "id": "design:core-agent-adapters-llm", + "label": "core/agent/ + adapters/llm 基础设施设计", + "type": "design" } ], "links": [] diff --git a/research-wiki/index.md b/research-wiki/index.md index 869b770..867eaaf 100644 --- a/research-wiki/index.md +++ b/research-wiki/index.md @@ -1,6 +1,10 @@ # Research Wiki 索引 -> 自动生成,更新时间:2026-07-06 15:13 UTC +> 自动生成,更新时间:2026-07-07 02:06 UTC + +## design (2) +- [2026-07-06-core-agent-adapters-llm-design](designs/2026-07-06-core-agent-adapters-llm-design.md) `design:2026-07-06-core-agent-adapters-llm-design` +- [core/agent/ + adapters/llm 基础设施设计](designs/core-agent-adapters-llm.md) `design:core-agent-adapters-llm` ## plan (1) - [项目基础设施初始化计划](plans/infrastructure-setup.md) `plan:infrastructure-setup` diff --git a/research-wiki/log.md b/research-wiki/log.md index ebf9161..7688d17 100644 --- a/research-wiki/log.md +++ b/research-wiki/log.md @@ -3,3 +3,5 @@ - [2026-07-06 13:54 UTC] Wiki 初始化完成 - [2026-07-06 15:09 UTC] 新增 plan: 项目基础设施初始化计划 (plan:infrastructure-setup) - [2026-07-06 15:13 UTC] 重建索引: 1 篇页面 +- [2026-07-07 02:06 UTC] 新增 design: core/agent/ + adapters/llm 基础设施设计 (design:core-agent-adapters-llm) +- [2026-07-07 02:06 UTC] 重建索引: 3 篇页面