Agent 抽象与三大流派(被评测的主角)
30 秒导读: 一个 benchmark 要能公平比较 Terminus、Claude Code、oracle 这些五花八门的东西, 前提是它们在 harness 眼里长一个样。本章讲清那个"一个样"是什么——
BaseAgent这份契约—— 再看三种完全不同的东西怎么各自钻进这份契约里 。
本章的位置:任务长什么样见 01-task-anatomy,一次 trial 怎么端到端跑见 02-harness-loop,agent 操作的那个终端沙箱见 03-terminal-tmux, 跑完怎么判分见 05-scoring-results。这里只讲被评测的主角本身。
1. 这是什么(零基础也能懂)
一句话定义: 在 Terminal-Bench 里,"agent" 就是一个会拿到任务指令、被丢进一个真终端、
然后想办法把任务做完的东西。它可以是一段自写的 LLM 循环,也可以是别人家的命令行工具(如
claude),甚至可以是"直接把标准答案跑一遍"的作弊基线。
为什么需要一层抽象? 想象你要给这么多参赛者排名次:
| 参赛者 | 它其实是什么 |
|---|---|
| Terminus | 项目自带的一段 Python,循环调 LLM、把命令敲进终端 |
| Claude Code | Anthropic 的现成 CLI,得先 npm install 装进容器再跑 |
| oracle | 根本不是 AI,直接执行任务作者写好的 solution.sh |
这三个从里到外都不一样。harness 不可能为每一个单独写一套跑法——那样加一个新 agent 就要 改一次 harness。解决办法是定一份契约:不管你内部多怪,只要你实现"给你指令和终端、你还我一个结果" 这一个方法,harness 就能跑你。
一句话直觉: 就像电源插座。插座(harness)不关心插头后面是台灯还是电钻,只认插头的形状
(perform_task)。三大流派 = 三种"插头后面的电器",但插头形状必须一样。
2. 顶层全景(契约 + 三大流派怎么共存)
怎么读这张图: 中间那根竖线是唯一的契约 perform_task。左边是 harness(它只透过契约看 agent),
右边三支是三种把自己塞进契约的方式。三支内部天差地别,但对 harness 的那个"插头"完全一样。
┌─ 契约 (base_agent.py) ─┐
harness 端 │ │ 三大流派(右侧)
(见 02 章) │ perform_task( │
│ instruction, │ ①进程内 LLM 循环
拿到 agent 实例 ───调用──▶│ session, ────────┼──▶ Terminus / Terminus2
│ logging_dir │ (自己 prompt→LLM→敲键→读屏)
◀── AgentResult ─────────│ ) -> AgentResult │
(token/failure/marker)│ │ ②容器内现成 CLI
│ ├──▶ ClaudeCodeAgent 等
AgentFactory │ name() version │ (装 CLI 进容器,跑 claude -p)
(按名字/import 造实例) │ prompt_template │
│ │ ③oracle / 基线
└────────────────────────┘──▶ OracleAgent (跑标准答案)
naive / nop (对照组)
部件一句 话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
BaseAgent | 定义契约:抽象方法 perform_task / name,属性 version / prompt_template | agents/base_agent.py:38 |
AgentResult | agent 交回的战报:token 数、失败模式、时间戳标记 | agents/base_agent.py:11 |
FailureMode | 失败原因枚举(超时 / 解析错 / 装不上等) | agents/failure_mode.py:4 |
AgentName | 内置 agent 的名字枚举(CLI --agent 用的就是这些字符串) | agents/agent_name.py:4 |
AgentFactory | 按名字或 import_path 造出 agent 实例(注册表 + 动态加载) | agents/agent_factory.py:36 |
3. 契约本身:BaseAgent 与 AgentResult
这节讲那份"插头形状"到底规定了什么。契约很短,就四样东西。
3.1 唯一的核心方法 perform_task
harness 跟 agent 打交道,从头到尾只调这一个方法(base_agent.py:124):
# 示意,非源码:契约签名
def perform_task(
self,
instruction: str, # 任务指令(任务的 task.yaml 里那段自然语言)
session: TmuxSession, # 一个活的终端会话,agent 往里敲键、从里读屏
logging_dir: Path | None, # 把轨迹/中间产物写哪
) -> AgentResult: # 交回战报
...
三个入参、一个返回,就是全部。agent 不自己起容器、不自己判分——容器已经由 harness 建好,
session 递进来即用;判分是 agent 交回后 harness 另外做的(见 05 章)。
这条边界是三大流派能共存的根:agent 只管"在终端里把事做了",别的都不归它。
session 是什么、send_keys / capture_pane 怎 么真正把键敲进 tmux,见
03-terminal-tmux。本章只需知道:agent 手里的终端就是这个对象。
3.2 交回的战报 AgentResult
AgentResult 是个 Pydantic 模型(base_agent.py:11),装四样:
| 字段 | 含义 | 默认 |
|---|---|---|
total_input_tokens | 这次任务累计输入 token | 0 |
total_output_tokens | 累计输出 token | 0 |
failure_mode | agent 自己知道的失败原因;不知道就填 NONE | FailureMode.NONE |
timestamped_markers | (时间戳, 文本) 列表,给 asciinema 录像打标签用 | [] |
关键细节:failure_mode 是 agent 的自述,不是判决。 多数 agent(包括跑成功的 Terminus)
返回的就是 NONE——因为"任务对没对"由测试脚本说了算,不由 agent 自称。agent 只在它自己确知
自己挂了时才填非 NONE:比如 naive agent 解析不了 LLM 回复就填 FATAL_LLM_PARSE_ERROR
(naive_agent.py:90),installed agent 装不上就填 AGENT_INSTALLATION_FAILED
(abstract_installed_agent.py:166)。全部可选值:
UNSET / NONE / UNKNOWN
TEST_TIMEOUT / AGENT_TIMEOUT / UNKNOWN_AGENT_ERROR
PARSE_ERROR / FATAL_LLM_PARSE_ERROR
CONTEXT_LENGTH_EXCEEDED / OUTPUT_LENGTH_EXCEEDED
AGENT_INSTALLATION_FAILED
(failure_mode.py:4)
3.3 另外两样:version 与 prompt_template
契约还带两个属性,都可选:
version(base_agent.py:52)——任意字符串,可由构造参数动态给。它的用处很具体: installed agent 的安装脚本用它锁版本,例如npm install -g @anthropic-ai/claude-code@{version}。prompt_template(base_agent.py:81)——一个 Jinja2 模板文件路径。给了它,_render_instruction(base_agent.py:104)就在把指令交给 agent 前先套一遍模板;没给就原样透传。这让你能不改代码、 只用--agent-kwarg prompt_template=...给同一个 agent 换外壳 提示词。
4. 注册与加载:AgentFactory + AgentName
这节讲:你在命令行敲 --agent terminus,那个字符串怎么变成一个真实的 Python 对象。
4.1 两种寻址方式
AgentFactory.get_agent(agent_factory.py:104)接受二选一的入参(给了两个或都不给都报错):
| 方式 | 参数 | 适合 |
|---|---|---|
| 按名字 | agent_name: AgentName | 内置 agent,查静态注册表 |
| 按导入路径 | import_path: "module:Class" | 库外自定义 agent,动态 import |
内置注册表是一个"名字→类"的字典,在类体里一次性建好(agent_factory.py:37):
# 示意,非源码:注册表就是把每个类的 name() 当 key
AGENT_NAME_TO_CLASS = {
agent.name(): agent
for agent in [OracleAgent, NaiveAgent, Terminus, Terminus2,
ClaudeCodeAgent, ...] # 全部内置 agent 列在这
}
AGENT_NAME_TO_CLASS["terminus"] = Terminus # 老名字 terminus 兼容到 terminus-1
注意 key 来自每个类的静态方法 name(),而 name() 通常返回 AgentName 枚举的 .value
(比如 AgentName.TERMINUS_1.value == "terminus-1")。所以枚举、name()、注册表三者要对得上,
枚举是这些字符串的单一事实源(agent_name.py:4)。
4.2 库外 agent:动态加载
get_agent_from_import_path(agent_factory.py:63)把 "my_pkg.my_mod:MyAgent" 拆成模块名和类名,
importlib.import_module 加载,getattr 取类,并强制校验它是 BaseAgent 子类才放行:
# 示意,非源码
module = importlib.import_module(module_name)
agent_class = getattr(module, class_name)
if not issubclass(agent_class, BaseAgent): # 不签契约的一律拒绝
raise ValueError(...)
这一步是整个抽象的闭环:任何人写的 agent,只要继承 BaseAgent 实现 perform_task,就能被同一套
harness 跑,无需改动 Terminal-Bench 一行代码。
5. 流派一:进程内 LLM 循环 —— Terminus
这是"最纯"的一支:agent 的大脑(LLM 循环)跑在 harness 进程里,通过 session 隔着容器边界
遥控终端。Terminus 是 Terminal-Bench 自带的参考 agent,也是理解"agent 到底在干嘛"的最佳样本。
5.1 它的一个回合在做什么
Terminus 把任务拆成一串 episode,每个 episode 是一次"想—做—看"的闭环:
初始 prompt(指令 + JSON schema + 当前屏幕)
│
▼
┌──────────── 一个 episode ────────────┐
│ 1. prompt 喂给 LLM │
│ 2. LLM 回一段 JSON (CommandBatchResponse)
│ 3. 解析成 Command 列表 │
│ 4. 逐条 send_keys 敲进终端 │◀── 隔着容器边界操作 tmux
│ 5. is_task_complete? ── 是 ──▶ 退出 │
│ 6. 否:capture_pane 抓屏 → 当下一轮 prompt
└───────────────── ─────────────────────┘
│(回到步骤 1,最多 max_episodes 轮)
▼
返回 AgentResult(token 累计)
主循环 _run_agent_loop(terminus_1.py:190)就是这张图的直译,perform_task(terminus_1.py:214)
负责拼初始 prompt 并起循环。
5.2 用 JSON schema 卡住 LLM 的嘴
Terminus 不让 LLM 自由发挥,而是要求它每轮都回一个固定结构的 JSON。结构由 Pydantic 模型
CommandBatchResponse 定义(terminus_1.py:49):
| 字段 | 作用 |
|---|---|
state_analysis | LLM 对当前终端状态的判断(逼它先观察再动手) |
explanation | 这批命令要干嘛的简述 |
commands | 一批 Command,逐条敲进终端 |
is_task_complete | LLM 自认为做完了没——这就是循环的退出信号 |
每个 Command(terminus_1.py:21)又带三个字段:keystrokes(要敲的键,支持 C-c 这种 tmux 转义)、
is_blocking(要不要等这条跑完再看输出)、timeout_sec(等多久)。
这个 schema 会被 model_json_schema() 序列化成文本,塞进给 LLM 的 prompt 里(terminus_1.py:92),
让模型照着填。prompt 模板见 agents/prompt-templates/terminus.txt——它明确告诉模型"你直接操作
tmux 会话,要执行命令得自己在末尾加 \n,遇到 less/vim 这类交互程序不要阻塞等待"。
5.3 敲键的两个魔鬼细节
命令怎么真正落到终端,在 _execute_commands(terminus_1.py:156)。两个不显然的处理值得记:
① is_blocking 会被二次否决。 即使 LLM 说这条要阻塞等待,只要 keystrokes 去空白后以 EOF
或 & 结尾,就强制不阻塞(terminus_1.py:170-180):
# 示意,非源码:终点是 EOF(here-doc) 或 &(后台) 的命令,永不阻塞
block = command.is_blocking and not (
command.keystrokes.strip().endswith("EOF")
or command.keystrokes.strip().endswith("&")
)
道理很实在:here-doc(cat <<EOF ... EOF)和后台任务(cmd &)本来就不会"自己结束并回吐输出",
若真去阻塞等它,会把整轮卡死到超时。这是踩过坑之后的防呆。
② 超时不是崩溃,是喂回给 LLM 的一段话。 某条命令 send_keys 抛 TimeoutError,
_execute_commands 不往上抛异常,而是返回一段"超时了 + 当前屏幕"的文本(用 timeout.txt 模板拼),
让下一轮 prompt 告诉 LLM"你上一条超时了,屏幕现在长这样",由模型自己决定怎么补救。
5.4 退出、重试、记录
- 退出:任一轮 LLM 回
is_task_complete=True,循环break(terminus_1.py:209);否则跑满max_episodes(默认 50,terminus_1.py:81)自然结束。注意:"做完"不等于"做对",退出后由测试脚本判分。 - 重试:
_handle_llm_interaction挂了tenacity的@retry,最多 3 次 (terminus_1.py:117),但上下文超长 / 输出超长这两类异常明确不重试(重试也没用,得换策略)。 - 记录:每轮把 LLM 的原始回复当作一条 asciinema marker 记下(
_record_asciinema_marker,terminus_1.py:238),日后能把"模型当时在想什么"和"屏幕录 像"对齐着看。
5.5 terminus_2 有什么不同(概览)
Terminus2(terminus_2/terminus_2.py:31)是同一流派的进阶版,思路一样但几处放开/加固,只做概览:
| 维度 | Terminus (1) | Terminus2 |
|---|---|---|
| 输出格式 | 严格 JSON,Pydantic 校验 | 可选 json / xml 的宽松 plain parser(terminus_2.py:69) |
| 回合上限 | 默认 50 | 默认近乎无限(1000000),显式限制反而会告警(terminus_2.py:53-60) |
| 上下文爆了怎么办 | 不重试,直接抛 | 回卷旧消息 + 自我总结再续(_query_llm, terminus_2.py:320) |
| 输出爆了怎么办 | 不重试 | 尝试从截断输出里"抢救"出合法响应,抢救不到就要求分块重发 |
| 完成确认 | 一次 is_task_complete 即退 | 双重确认:先问"你确定?",再答确定才真退(terminus_2.py:572) |
一句话:Terminus2 把 Terminus 那些"撞墙就抛异常"的地方,换成了"撞墙就自我修复继续跑"。
6. 流派二:装进容器的现成 CLI —— AbstractInstalledAgent
第二支解决的是另一类需求:我想评测一个别人做好的命令行 agent(claude、aider、codex……),
它有自己的一套交互逻辑,没法用我们外部遥控终端那套。 办法是把这个 CLI 装进任务容器里,
让它在容器内部自己跑。
6.1 为什么这支"是下策但必要"
源码开头的 docstring 说得很直白(abstract_installed_agent.py:1-11):这种方式给容器塞了额外依赖,
可能因为容器环境问题(卷约束、网络坏)而失败,而非 agent 本身不行,所以是 last resort。
但要评测现成 CLI,又绕不开它。
6.2 一次 installed-agent 任务的四步
perform_task(abstract_installed_agent.py:108)是模板方法,固定四步:
1. 拷脚本进容器 copy_to_container(install-agent.sh → /installed-agent/)
│
2. 写环境变量文件 把 _env(如 ANTHROPIC_API_KEY)写成 setup-env.sh
│ ——刻意在 session 外用 exec_run 写,避免 key 出现在终端录像里
│ source setup-env.sh
▼
3. 装 agent source install-agent.sh || echo 'INSTALL_FAIL_STATUS'
│ 抓屏检查有没有 INSTALL_FAIL_STATUS
│ └─ 有 → 直接返回 failure_mode=AGENT_INSTALLATION_FAILED
▼
4. 跑 agent _run_agent_commands(instruction) 逐条 send_command
三个设计点:
- 密钥不进录像:环境变量文件用
container.exec_run在会话之外写入 (abstract_installed_agent.py:122),注释明说"避免把 env 变量暴露在会话里"。 - 装不上是可识别的失败:安装命令后面
|| echo 'INSTALL_FAIL_STATUS',然后抓屏找这个魔法字符串 (abstract_installed_agent.py:144-160)。找到就返回装配失败,不会把"环境没装好"误判成"agent 太笨"。 - 子类只需填三样:
_env(要注入的环境变量)、_install_agent_script_path(安装脚本)、_run_agent_commands(怎么带着指令启动 agent)。骨架全在基类。
6.3 实例:ClaudeCodeAgent
ClaudeCodeAgent(installed_agents/claude_code/claude_code_agent.py:12)就是把上面三个钩子填上:
_env(claude_code_agent.py:39):注入ANTHROPIC_API_KEY(从 harness 宿主环境读), 可选把model_name去掉anthropic/前缀后作为ANTHROPIC_MODEL,另开两个后台任务开关。- 安装脚本:
claude-code-setup.sh.j2——装 nvm、装 node 22,再npm install -g @anthropic-ai/claude-code@{{ version }},version就来自契约里的version属性。 _run_agent_commands(claude_code_agent.py:55):拼出真正的启动命令,核心是claude -p(print/非交互模式),把指令shlex.quote转义后传入,并用--allowedTools把可用工具限死在一张 白名单里(Bash/Edit/Write/Read/Glob/Grep/...,claude_code_agent.py:17):
# 示意,非源码:最终在容器里跑的那条命令
claude --verbose --output-format stream-json \
-p '<转义后的任务指令>' \
--allowedTools Bash Edit Write Read Glob Grep LS ...
对比流派一:这里 Terminal-Bench 完全不管 claude 内部怎么循环、怎么调模型——它只负责把 CLI 装好、
喂一句指令、等它跑完。agent 的"大脑"这次跑在容器里,不在 harness 进程里。
7. 流派三:oracle 与对照基线
第三支根本不是 AI,存在的意义是给分数定标尺。
7.1 OracleAgent:满分应该长什么样
每个任务都自带作者写好的参考解(solution.sh 或 solution.yaml,见 01 章)。
OracleAgent(oracle_agent.py:27)做的事就是把这个标准答案跑一遍:
- 构造时遍历数据集,把每个任务的"指令 → 参考解"建成一张表(
_init_solution_dict,oracle_agent.py:50)。 perform_task(oracle_agent.py:73)按指令查表,然后分两种执行(oracle_agent.py:81):
| 解的类型 | 怎么跑 |
|---|---|
.sh | 把 solution.sh 拷进容器,bash /oracle/solution.sh |
.yaml | 逐条把 TerminalCommand send_command 进终端 |
它的用途: oracle 跑出来应该是满分基线。如果连 oracle 都过不了某个任务,说明这个任务的 测试脚本或参考解本身有 bug——oracle 是任务质量的体检工具,不是参赛选手。
7.2 naive 与 nop:另外两根标尺
NaiveAgent(naive_agent.py:23):只调一次 LLM,让它一口气吐出所有命令,然后挨条敲进去, 不看任何反馈、没有循环。它是"最笨的 LLM 用法"下限——用来衬托 Terminus 那套"看反馈再动手"的循环 到底值多少分。解析不出 LLM 回复时它诚实上报FATAL_LLM_PARSE_ERROR(naive_agent.py:90)。NopAgent(null_agent.py:7):什么都不做,直接返回NONE。这是绝对零分线——如果一个任务 连"啥也不干"都能过,那这任务的测试脚本形同虚设。
三根标尺合起来:nop(下限)→ naive(笨 LLM)→ 真实 agent → oracle(上限),任何真实 agent 的分数
都应落在这段区间里,落在区间外就是信号:要么任务坏了,要么测试太松。
一句带过 MCP 流派: 还有个
MCPTerminus(mcp_agents/mcp_terminus.py:32),走 Model Context Protocol:不再解析文本命令,而是把终端能力暴露成 MCP 工具、用 LLM 的原生 tool-calling 调用。 本章不展开,属于第一支的变体。
8. LLM 适配层:Chat 与 LiteLLM
流派一/三里的 LLM 调用,都走同一层薄封装。这节讲这层在契约之下多做了什么。
8.1 Chat:管对话历史与 token 累计
Chat(llms/chat.py:6)包住一个 BaseLLM,做两件事:攒消息历史、累加 token 计数。
关键细节在计数方式——它用的是累计计数器,而不是每次从当前消息重算(chat.py:15):
# 示意,非源码:为什么要单独攒累计值
self._cumulative_input_tokens += input_tokens # 只增不减
# ——因为像 terminus_2 会中途删掉旧消息来腾上下文,
# 若从"当前消息"重算,被删掉那部分的 token 账就丢了
这样即使后面把旧消息裁掉(terminus_2 的回卷机制),花掉的 token 账依然算数,AgentResult 里的
token 统计才诚实。
8.2 BaseLLM 的三个异常
BaseLLM(llms/base_llm.py:24)只规定 call 和 count_tokens 两个抽象方法,但它同文件定义了三个
贯穿整套 agent 的异常类型,是错误处理的通用语言:
| 异常 | 什么时候抛 | 谁在乎 |
|---|---|---|
ContextLengthExceededError | 上下文窗口塞满了 | Terminus 不重试、Terminus2 触发总结回卷 |
OutputLengthExceededError | 回复被 max_tokens 截断(还带回截断内容) | Terminus2 尝试抢救截断响应 |
ParseError | 回复解析不成目标结构 | Terminus 把它计入重试 |
8.3 LiteLLM:统一几十种模型的差异
LiteLLM(llms/lite_llm.py:35)是 BaseLLM 的实测实现,底层用 litellm 库对接各家模型。它替上层
抹平了几处厂商差异:
- 探测能力再降级:构造时用
get_supported_openai_params查这个模型支不支持结构化输出 (response_format)和temperature(lite_llm.py:50-55)。若不支持response_format,就把 JSON schema 塞进 prompt 文字里、让模型"照着写",而不是用 API 的结构化字段(lite_llm.py:132-138)。 - Anthropic 提示缓存:对 Anthropic/Claude 系模型,给最近 3 条消息打
cache_control: ephemeral标记(add_anthropic_caching,lite_llm.py:149→utils/anthropic_caching.py:7),省重复前缀的钱。 - 把厂商异常翻成自家异常:捕获
litellm的上下文超限异常,重抛成上面那个统一的ContextLengthExceededError(lite_llm.py:164);发现finish_reason == "length"就抛OutputLengthExceededError并附上被截断的内容供上层抢救(lite_llm.py:175)。 - 退避重试:
call挂了指数退避@retry(最多 3 次,lite_llm.py:113),但上下文超限 / 输出超限 / 鉴权错这三类不重试(重试无意义或会泄露密钥问题)。 - 暂不支持流式:拿到
CustomStreamWrapper直接NotImplementedError(lite_llm.py:170)。
一句话总结这层:上面的 agent 循环只跟"发一句、回一句、超了会抛标准异常"打交道,各家模型的脾气差异
全被 LiteLLM 关在门内。
9. 边界与局限(诚实)
- "任务完成"由 agent 自称,"任务正确"由测试脚本判:
is_task_complete/failure_mode=NONE都 只代表 agent 认为自己做完了,不代表做对了。真正的判分在 agent 交回之后(05 章)。 - installed-agent 支是脆的:它把成败和容器环境(网络、卷、apt/npm 能否装)绑在一起,源码自己都
标注"last resort"(
abstract_installed_agent.py:1-11)。 - 流式未支持:
LiteLLM明确对流式响应抛NotImplementedError(lite_llm.py:170)。 - oracle 依赖参考解的质量:oracle 只是"跑作者写的答案",答案本身错了它也跟着错——它体检的是 任务,不是 agent。
10. 代码地图(导航索引)
| 主题 | 文件路径 | 关键符号 |
|---|---|---|
| 契约:核心方法与结果 | terminal_bench/agents/base_agent.py | BaseAgent / perform_task / AgentResult / _render_instruction |
| 失败模式枚举 | terminal_bench/agents/failure_mode.py | FailureMode |
| 内置 agent 名字枚举 | terminal_bench/agents/agent_name.py | AgentName |
| 注册表 + 动态加载 | terminal_bench/agents/agent_factory.py | AgentFactory / AGENT_NAME_TO_CLASS / get_agent_from_import_path / get_agent |
| 流派①:LLM 循环 | terminal_bench/agents/terminus_1.py | Terminus / CommandBatchResponse / Command / _run_agent_loop / _execute_commands |
| 流派①的 prompt | terminal_bench/agents/prompt-templates/terminus.txt | (模板文本) |
| 流派①进阶版 | terminal_bench/agents/terminus_2/terminus_2.py | Terminus2 / _query_llm / _summarize |
| 流派②:容器内 CLI 骨架 | terminal_bench/agents/installed_agents/abstract_installed_agent.py | AbstractInstalledAgent / perform_task / _run_agent_commands |
| 流派②实例:Claude Code | terminal_bench/agents/installed_agents/claude_code/claude_code_agent.py | ClaudeCodeAgent / _env / ALLOWED_TOOLS |
| 流派③:满分基线 | terminal_bench/agents/oracle_agent.py | OracleAgent / _init_solution_dict |
| 对照基线 | terminal_bench/agents/naive_agent.py, null_agent.py | NaiveAgent / NopAgent |
| MCP 变体(未展开) | terminal_bench/agents/mcp_agents/mcp_terminus.py | MCPTerminus |
| LLM 适配:对话与计数 | terminal_bench/llms/chat.py | Chat / total_input_tokens |
| LLM 适配:抽象与异常 | terminal_bench/llms/base_llm.py | BaseLLM / ContextLengthExceededError / OutputLengthExceededError / ParseError |
| LLM 适配:LiteLLM 实现 | terminal_bench/llms/lite_llm.py | LiteLLM / call / count_tokens |
| Anthropic 提示缓存 | terminal_bench/utils/anthropic_caching.py | add_anthropic_caching |