Files
Video-Tree-TRM5/research-wiki/designs/2026-07-06-core-agent-adapters-llm-design.md
T
iomgaa f541c4047a design: core/agent/ + adapters/llm 基础设施设计
全异步 AgentLoop + 四层治理栈(超时→重试→熔断→缓存)+ 流式三层看门狗。
LLMProvider/VLMProvider/TelemetryRecorder 上提到 core/protocols.py 作为共享端口。
经 Codex 独立审查,修复 7 项发现(含缓存遥测、inter_token 指标、json_repair 依赖等)。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-06 22:06:48 -04:00

16 KiB
Raw Blame History

core/agent/ + adapters/llm 基础设施设计

日期 2026-07-06 · 状态 已批准 · 涉及算法保真 #11 Agent Loop


§1 设计决策总结

决策点 结论 理由
同步 vs 异步 全异步 流式 SSE + asyncio.timeout 看门狗 + 未来 ARQ 扩展
LLMProvider 返回值 完整 LLMResponseadapters 内部流式消费) core 不需逐 token 处理;TTFT 是基础设施指标属 adapters 层
thinking 剥离层 adapters 层统一剥离 core 不感知 provider 差异;新增 provider 只改 adapters
遥测写入 两层写入 adapters: 原始 LLM 指标 → telemetry.dbGovernedLLMClient 自动);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 RedisResponseCachecontent-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 依赖方向

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

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_idepoch/step/question 关联,遥测链路追踪
  • parent_call_idagent step → LLM call 父子关系
  • 无 model/temperature——实例配置,构造时确定

VLMProvider

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

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 原方案新增 thinkingttft_msmax_inter_token_ms 字段。全部 keyword-only。


§4 core/types.py — LLMResponse

@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

@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 TRM4Step 新增 call_id,其余结构保持一致(算法保真 #11)。

5.2 core/agent/protocols.py

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;新增 contextasync 化。无效工具名抛 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

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/ 允许依赖 stdlibtypingpluggyjson_repairjson_repair 是 AgentLoop 算法保真 #11 的必要依赖(TRM4 已使用),作为 core 允许的第三方库显式列入。


§6 adapters/ — 四层治理栈

6.1 adapters/streaming.py — 三层看门狗

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 tokenis_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 — 熔断器

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 — 响应缓存

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-addressedkey = sha256(model + json_canonical(messages))
  • Redis 不可用时静默降级(get 返回 Noneset 吞异常 + 日志告警)
  • TTL 通过 .env 工程配置管理

6.4 adapters/telemetry.py — 遥测记录

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

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 <think> 标签剥离 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.pyadapters/ 新增 streaming.pybreaker.py
ARCHITECTURE.md §2.4 依赖方向 core/ 允许依赖扩展为:标准库、typing、pluggy、json_repair
ARCHITECTURE.md §3.1 核心端口 LLMProvider/VLMProvider/TelemetryRecorder 从 core/agent/protocols.pycore/evolution/protocols.py 上提到 core/protocols.pyToolDispatcher/AgentLoopSpec 留在 core/agent/protocols.py
ARCHITECTURE.md §4 遥测规范 新增 thinkingttft_msmax_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_TIMEOUTLLM_INTER_TOKEN_TIMEOUTREDIS_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+ 行单文件,组件无法独立测试