Solver 与 TaskState:可组合的求解步骤
30 秒导读: 一条评测样本从"初始 prompt"走到"可打分的最终答案",中间的过程由一串 Solver(求解步骤)负责。它们共享一个可变的状态盒子 TaskState——里面装着对话历史、模型输出、工具、是否结束等。每个 Solver 拿到状态、改一改、传给下一个;
generate()(调一次模型)是默认的那一步,basic_agent()则把"想— 调工具—看结果"包成一个最小智能体循环。
本章讲过程侧。样本怎么被读进来、怎么调度、怎么打分,分别在 01-eval-loop、05-scorer-metrics;模型层在 03-model-layer;功能更全的 react 智能体在 04-tools-agents。这里只盯住:一串 Solver 如何把 TaskState 变换到 output。
1. 这是什么(零基础也能懂)
一句话定义: Solver 是"求解一条评测样本的一个步骤";TaskState 是这些步骤之间传来传去的"状态盒子"。
它解决什么问题。 评测一个大模型,很少是"把问题丢给模型、拿回答案"这么直。你常要:先塞一段系统提示、把题目套进模板、给模型装几把工具、让它反复调用工具直到给出答案、有时还要多选题按字母判分。把这些步骤拆成可插拔的小块、再串起来,就是 Solver 要干的事。
一句话直觉。 把它想成 Unix 管道:
初始 TaskState ──▶ [系统提示] ──▶ [装工具] ──▶ [调模型] ──▶ 最终 TaskState
每个方块 = 一个 Solver,盒子 = TaskState 一路被改
用起来什么样。 一个任务(Task)的 solver 参数就是一串 Solver;不给就默认只有一步 generate():
# 示意,非源码:一个任务把三个 solver 串成求解过程
Task(
dataset=my_dataset,
solver=[
system_message("你是一个严 谨的数学助教"), # 第 1 步:插系统提示
use_tools([calculator()]), # 第 2 步:装工具
generate(), # 第 3 步:调模型(默认这步)
],
scorer=match(),
)
读者读完本节只需记住:Solver = 一个步骤,TaskState = 步骤间的共享状态,串起来 = 求解过程。
2. 顶层全景(它大概怎么转)
2.1 两个协议 + 一个状态
整个过程侧只有三个主角:
| 主角 | 是什么 | 定义位置 |
|---|---|---|
Solver | 一个 async 可调用:(state, generate) -> state,即"改状态的一步" | solver/_solver.py:77 Solver |
Generate | 一个 async 回调:"调一次模型、把回复追加进状态",发给每个 Solver 用 | solver/_solver.py:36 Generate |
TaskState | 步骤间传递的可变状态盒子(对话、输出、工具、是否完成…) | solver/_task_state.py:139 TaskState |
注意一个容易忽略的设计:Solver 自己不知道怎么调模型。调模型的能力由运行器从外面注入——就是那个 generate 参数。这样同一个 Solver,在不同 provider / 不同并发调度下都能复用(注入点见 §5.2)。
2.2 一条样本的过程侧全景
┌─────────────────────────────────────────┐
Sample(题目) ──▶ │ TaskState(状态盒子) │
│ messages / output / tools / store / │
│ completed / choices / target ... │
└─────────────────────────────────────────┘
│ 被依次传入
▼
task.solver ──resolve──▶ Plan([ solver_1, solver_2, ..., generate() ])
│ for each solver:
│ state = await solver(state, generate)
│ 若 state.completed → 提前跳出
▼
最终 TaskState.output ──▶ 交给 Scorer 打分(→ 05 章)
怎么读这张图: 从上到下是数据流。Sample 先被包成 TaskState;task 的 solver 列表被解析成一个执行器(内部是 Plan),按顺序把 state 喂给每个 solver;任何一步把 completed 置真就提前收尾;最后拿 output 去打分。
各部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
resolve_solver / resolve_plan | 把用户给的 solver 列表/单个 solver 统一成一个执行器 | _eval/task/task.py:474、_eval/task/run.py:279 |
Plan / Chain | 顺序执行一串 solver,completed 时提前退出 | solver/_plan.py:21、solver/_chain.py:53 |
generate 闭包 | 运行器造好、注入给每个 solver 的"调模型"回调 | _eval/task/run.py:539 |
task_generate | 那个回调背后真正干活的:调模型 + 跑工具循环 | _eval/task/generate.py:11 task_generate |
3. 核心原理(逐个机制,由浅入深)
3.1 Solver 协议:一个改状态的 async 步骤
要解决的小问题: 怎么定义"一步"才能既灵活又能互相串联?
思路: 定成一个结构化协议(Protocol),而不是继承某个基类——只要你是"接收 (state, generate)、返回 state 的 async 可调用",你就是个 Solver。函数、类实例都行。
真实定义(solver/_solver.py:77 Solver):
@runtime_checkable
class Solver(Protocol):
async def __call__(
self,
state: TaskState,
generate: Generate,
) -> TaskState: ...
一句话:契约就是"拿状态、可选地调 generate、还回状态"。Solver 可以只做 prompt 工程(改改消息就返回),也可以真去调模型。
怎么写一个 Solver: 用 @solver 装饰一个"返回 Solver 的工厂函数"。外层函数收配置参数,内层 solve 才是真正那一步。
# 示意,非源码:一个最简单的 prompt 工程 solver
@solver
def prompt_cot(template: str) -> Solver:
async def solve(state: TaskState, generate: Generate) -> TaskState:
state.user_prompt.text = template.format(prompt=state.user_prompt.text)
return state # 只改 prompt,没调模型
return solve
这个"工厂 + 内层 solve"的两层结构,是所有内置 solver 的统一写法(对照 solver/_prompt.py:142 chain_of_thought,几乎一模一样)。
3.2 @solver 装饰器:注册 + 状态追踪注入
要解决的小问题: 装饰器不只是登记名字,它还偷偷干了两件必须的事。
其一,注册到 registry,这样 solver 能被按名字创建(CLI/配置里用字符串指定 solver)。名字解析、registry_tag 都在 solver_wrapper 里(solver/_solver.py:194)。
其二, 也是关键细节:每次 solver 跑完,自动把最新 state 存进一个 ContextVar,方便其它地方(如打分、fork)随时取"当前样本状态"。看函数式 solver 的包裹(solver/_solver.py:226-232):
@wraps(solver)
async def registered_solver(state, generate):
state = await solver(state, generate)
set_sample_state(state) # 每步跑完,登记为"当前样本状态"
return state
set_sample_state / sample_state 就是那对 ContextVar 存取器(solver/_task_state.py:448-456)。对类形式的 solver(如 Chain),它改用打补丁 __call__ 的方式注入同样逻辑(solver/_solver.py:208-220)——因为要保留类型,好让别处 isinstance 还能认出 Chain/Plan。
SolverSpec 与按名创建。 SolverSpec(solver/_solver.py:63)记录"solver 名字 + 参数",用于从配置/CLI 重建 solver;名字可以是简单名,也可以是 file.py@name(从某文件里取某个 solver)。solver_create(name, **kwargs)(solver/_solver.py:131)据此从 registry 造出实例。