跳到主要内容

Burr — 不可变 State 与 Action(两大基石)

本章讲两块支撑整个 Burr 的基石。读完你能看懂任何一个 Burr 动作:它怎么读状态、怎么算、怎么把结果安全地并回一个新状态

1. State:一个永不被原地改的字典

它要解决的小问题

状态机里「当前状态」被无数动作读写,最怕的是:某个动作偷偷把别人的数据改了,或者你想「回到上一步的状态」却发现它已经被覆盖。Burr 的答案是:State 不可变——任何「修改」都返回一个新的 State 对象,旧的原封不动。

思路/直觉

State 实现了 Mapping(像只读字典),内部就一个 self._state 普通 dict(burr/core/state.py:273 class State)。它的 update/append/increment/extend/wipe 全都不改 self._state,而是:

  1. 把这次变更包成一个 StateDelta 对象;
  2. 拷一份内部 dict,在拷贝上施加变更;
  3. 用拷贝构造并返回一个新 State

图示:一次 update 的数据流

state.update(a=2, b=3)


SetFields({"a":2,"b":3}) ← 把变更包成一个 delta 对象
│ apply_operation

copy.copy(self._state) ← 浅拷内部 dict(read 到的键再 deepcopy)
│ operation.validate(...) ← 类型校验(如 append 必须是 list)
│ operation.apply_mutate() ← 在【拷贝】上原地改

State(新 dict) ← 返回全新 State,旧的不动

真实实现

核心就一个方法 apply_operation(burr/core/state.py:302):它浅拷内部 dict,对「读到的键」做 deepcopy 防别名,然后让 delta 自己校验 + 施加变更,最后包成新 State 返回。

# 真实源码节选 burr/core/state.py:302-319 State.apply_operation
new_state = copy.copy(self._state)
for field in operation.reads():
if field in new_state:
new_state[field] = copy.deepcopy(new_state[field]) # 防止共享引用被改
operation.validate(new_state)
operation.apply_mutate(new_state)
return State(new_state, typing_system=self._typing_system)

而面向用户的 update/append/increment 只是各包一个对应的 delta(burr/core/state.py:363 updateSetFields;:378 appendAppendFields;:414 incrementIncrementFields)。

StateDelta:为什么不直接改 dict

每种操作是一个 StateDelta 子类(burr/core/state.py:71 class StateDelta),都实现 reads()/writes()/apply_mutate(),还有 serialize/deserialize:

delta 类name干什么关键校验
SetFieldsset覆盖/新增键
AppendFieldsappend往 list 追加一个元素目标必须可 append(state.py:167)
ExtendFieldsextend往 list 批量追加目标必须可 extend(state.py:203)
IncrementFieldsincrement整数自增目标必须是 int(state.py:229)
DeleteFielddelete删键

为什么搞这么一层? 因为 delta 是可序列化的变更记录。代码注释直接点出了野心:目前是「每步都实打实算出整个 dict」,但 delta 模型给未来留好了「事件溯源式、只存增量历史」的口子(burr/core/state.py:307-310 注释 + issue #33)。

关键细节/坑

  • append 到非 list 会报错,且报错信息会教你「请确保字段是 list-like」(burr/core/state.py:167-176 AppendFields.validate)。
  • 取不存在的键,__getitem__ 抛的 KeyError 会贴心提示「是不是上游动作没产出 / 你没在 reads 里声明」(burr/core/state.py:455-462)。这正是 reads/writes 契约的「教学型报错」。

2. Action:一个动作 = 算(Function)+ 并(Reducer)

它要解决的小问题

一个动作要做两件事:用当前状态算出点东西(可能调 LLM),再把这点东西并回状态。Burr 把这两件事拆成两个接口,让框架能在中间插校验、追踪、流式等逻辑。

思路/直觉:两个抽象接口

Action
┌────┴─────┐
Function Reducer
(算) (并)
.reads .writes
.run() .update(result, state)
────────────────────────
run() 返回一个普通 dict;
update() 拿这个 dict + 旧 state,
返回【新 state】。
  • Function(burr/core/action.py:184):有 reads 属性 + run(state, **kwargs) -> dict
  • Reducer(burr/core/action.py:262):有 writes 属性 + update(result, state) -> State
  • Action(burr/core/action.py:306)= 同时继承两者 + 一个 name

「Reducer」这个名字来自函数式/Redux:拿(旧状态, 一个事件)产出新状态

两类动作:多步 vs 单步

这是最容易绕晕的地方,先看对照:

维度多步动作(class-based)单步动作(@action 函数)
你实现什么run()update() 分开写一个函数,返回新 State(或 (dict, State))
内部类直接的 Action 子类SingleStepActionFunctionBasedAction
执行入口_run_function + _run_reducer 两次调run_and_update 一次搞定
典型用途需要精细控制、复用 run/update99% 的日常写法

单步(single-step)指 run + update 一次返回(burr/core/action.py:713 class SingleStepAction,其 single_step 属性为 True)。@action 装饰器产出的就是它。

@action 装饰器到底做了什么

这是用户最常碰的 API。@action(reads=[...], writes=[...]) 是个类(burr/core/action.py:1438 class action),它的 __call__ 把一个 FunctionBasedAction 实例挂到函数对象上(属性名 action_function),并塞一个 bind 方法:

# 真实源码 burr/core/action.py:1493-1500 action.__call__
def __call__(self, fn) -> FunctionRepresentingAction:
setattr(fn, FunctionBasedAction.ACTION_FUNCTION,
FunctionBasedAction(fn, self.reads, self.writes, tags=self.tags))
setattr(fn, "bind", types.MethodType(bind, fn))
return fn

稍后 GraphBuilder.with_actions 会用 create_action(burr/core/action.py:1595)把这个挂着的 FunctionBasedAction 取出来、with_name 命名。

FunctionBasedAction.run_and_update(burr/core/action.py:882)就是直接调你的函数:

# 真实源码 burr/core/action.py:882-883
def run_and_update(self, state: State, **run_kwargs) -> tuple[dict, State]:
return self._fn(state, **self._bound_params, **run_kwargs)

原理演示:手写一个等价的「单步动作」

# 示意,非源码 —— 演示 @action 背后等价的「算+并」一次完成
def ai_response(state, prompt): # state: State,prompt 是运行时输入
answer = call_llm(prompt) # 算
new_state = state.update(response=answer) # 并(返回新 State)
return {"response": answer}, new_state # (结果 dict, 新 State)
# @action(reads=[], writes=["response"]) 把它包成 FunctionBasedAction

重点看:返回的是 (结果dict, 新State);若只返回 State,框架也会适配(见下「输出归一化」)。

reads/writes 是契约,不是注释

声明的 writes 会被强制:动作若写了没声明的键,执行时报错(burr/core/application.py:243 _run_reducer 里算 extra_keys 并抛错)。更狠的是,新版还会静态扫描函数源码:用 AST 找 state["x"] 这种下标读,如果 "x" 不在 reads 里,建图时就报错(burr/core/action.py:55 _validate_declared_reads,在 FunctionBasedAction.__init__ 调用,action.py:824)。

几个内置便捷动作

动作用途位置
Result(*fields)从状态里挑几个键当「最终结果」,不改状态burr/core/action.py:657
Input(*fields)把外部输入直接写进状态burr/core/action.py:683

3. Condition:转移边上的判断

它要解决的小问题

从一个动作出来可能有多条边(「如果要查数据库走 A,否则走 B」)。Condition 就是边上的判断器:读状态,返回 {"PROCEED": True/False}

三种写法

# 示意,非源码 —— 三种等价表达「safe == False」之类条件
from burr.core import when, expr
when(safe=False) # 字典式:state["safe"] == False
when(age__gte=18) # Django 风格后缀:state["age"] >= 18
expr("safe == False") # 表达式式:解析字符串,提取变量名做 key
  • when(**kwargs)(burr/core/action.py:524)支持 __gte/__lt/__in/__contains/__is 等后缀算子(算子表在 action.py:489 _OPERATORS),多个条件 AND 在一起。
  • expr(s)(burr/core/action.py:412)用 ast 解析表达式、自动收集变量名当 reads,再 eval注意它的安全警告:不要把用户输入喂给 expr(action.py:417 docstring)。
  • 条件可用 |(或,action.py:596)、&(与,action.py:617)、~(取反,action.py:644)组合。

default:兜底边

Condition.default(burr/core/action.py:648)是一个恒为 True、名为 "default" 的条件。没写条件的转移边就用它。建图时还会校验「一个动作不能有两条 default 边」(burr/core/graph.py:72-78 _validate_transitions)。

4. 输出归一化:为什么你可以「随便返回」

@action 函数可以返回 State、也可以返回 (dict, State)。框架在 _adjust_single_step_output(burr/core/application.py:111)把它统一成 (result_dict, new_state):如果你只返回了 State,它就把 result 当空 dict。这就是为什么文档里的例子有的 return state.update(...)、有的 return {...}, state.update(...),两种都行。

5. 代码地图(导航索引)

主题文件符号名
不可变状态容器burr/core/state.pyStateState.apply_operation
变更记录(delta)基类burr/core/state.pyStateDelta
各种变更操作burr/core/state.pySetFieldsAppendFieldsIncrementFieldsDeleteField
用户级状态操作burr/core/state.pyState.updateState.appendState.incrementState.wipeState.merge
动作两半接口burr/core/action.pyFunctionReducerAction
单步动作(函数式底座)burr/core/action.pySingleStepActionFunctionBasedAction
@action 装饰器burr/core/action.pyaction(class)、create_actionbind
reads 静态校验burr/core/action.py_validate_declared_reads
转移条件burr/core/action.pyConditionCondition.whenCondition.exprCondition.default
内置便捷动作burr/core/action.pyResultInput