12-Factor Agents 之五:统一执行状态与业务状态(Unify Execution State and Business State)
文档教程人工智能大模型AI Agent【免费下载链接】12-factor-agentsWhat are the principles we can use to build LLM-powered software that is actually good enough to put in the hands of production customers?项目地址https://gitcode.com/GitHub_Trending/12/12-factor-agents点击查看免费下载在 12-factor-agents 项目中Factor 5 回答了一个几乎所有 Agent 工程都会遇到的问题到底要不要把当前执行到哪一步执行状态和已经发生了什么业务状态分开管理本文以该原则为骨架结合仓库中create-12-factor-agent模板的真实源码src/agent.ts、src/state.ts、src/server.ts与 workshop 09 的实现深入讲解用一条线程Thread统一承载全部状态的工程手法以及由此带来的序列化、调试、恢复、分叉、可观测性等七大收益帮助读者在自己的 Agent 产品中落地这一设计。一、问题背景为什么基础设施系统总爱拆分两种状态在 AI 之外的传统软件世界里很多基础设施系统都习惯于把执行状态execution state与业务状态business state分开管理。这种做法的典型形态是用一张数据库表记录任务当前处于哪个步骤、下一步是什么、是否在等待、重试了几次再用另一套存储或消息流保存业务上真正发生的数据。受这套成熟思路影响不少 AI 应用的架构师在构建 Agent 时也会自然而然地引入复杂的抽象来跟踪当前步骤current step下一步骤next step等待状态waiting status重试次数retry counts以及其他一切流程编排元数据原文档content/factor-05-unify-execution-state.md明确指出这种分离会带来复杂度creates complexity它可能在某些场景下值得但对你的用例而言很可能过度设计overkill。最终选择权当然在你手里——但你不必认为自己必须把它们分开管理。二、两种状态的精确定义原文档给出了非常清晰的两类状态定义这也是全文的基石状态类别含义典型例子执行状态Execution stateAgent 工作流进行到哪一步的流程元数据当前步骤、下一步骤、等待状态、重试次数业务状态Business stateAgent 工作流到目前为止发生了什么的事实记录OpenAI messages 列表、工具调用tool calls及其结果的列表一句话概括二者的关系执行状态通常只是到目前为止发生了什么的元数据metadata about what has happened so far。三、核心主张能统一就统一SIMPLIFY原文档给出的建议非常直接If possible, SIMPLIFY - unify these as much as possible.如果可能就简化——尽可能把二者统一起来。为什么可以统一关键在于一个判断在现实中你可以通过精心设计应用让所有执行状态都能从上下文窗口context window中推断出来。换句话说当前在哪一步、是否在等待、接下来要做什么……这些信息本质上都可以从已经发生的事件序列中推导得出而不需要一个并行的独立状态机来单独维护。你仍然可能拥有一些放不进上下文窗口的东西例如会话 IDsession ids密码上下文password contexts其他运行时的敏感/环境信息但原文档强调你的目标应该是把这些例外最小化your goal should be to minimize those things。并且通过拥抱 Factor 3Own your context window你可以完全掌控实际进入 LLM 的内容——这为从上下文窗口推断执行状态提供了前提保障。3.1 动画示意原文档配了一张状态统一动画img/155-unify-state-animation.gif直观展示了执行状态从上下文窗口中长出、又随上下文一起持久化的循环状态被序列化后存入存储任何时刻都能仅凭一个状态 ID 恢复resume或分叉fork整条会话。四、统一状态带来的七大收益逐条展开原文档列出的好处共有七条。下面结合仓库源码逐一展开每一条都能在模板代码中找到直接对应物。1. 简单性Simplicity单一事实来源One source of truth for all state——所有状态只有一个源头那条线程Thread。没有两套存储、没有同步问题、没有流程表和事实表对不上的 bug。在仓库模板 packages/create-12-factor-agent/template/src/agent.ts 中Thread类的全部状态就是一条events数组export interface Event { type: string data: any; } export class Thread { events: Event[] []; constructor(events: Event[]) { this.events events; } // ... 序列化、等待判定等方法 }一切用户消息、LLM 决策、工具调用、工具结果、人类响应、错误都作为Event追加到events中没有第二个状态容器。这就是单一事实来源的字面实现。2. 序列化Serialization线程可以平凡地序列化/反序列化The thread is trivially serializable/deserializable——线程只是一个事件数组天然可以JSON.stringify落盘、JSON.parse恢复。模板中的持久化实现 packages/create-12-factor-agent/template/src/state.ts 干脆利落地印证了这一点export class FileSystemThreadStore implements ThreadStore { async create(thread: Thread): Promisestring { await fs.mkdir(this.threadsDir, { recursive: true }); const id crypto.randomUUID(); const filePath path.join(this.threadsDir, ${id}.json); const txtPath path.join(this.threadsDir, ${id}.txt); await Promise.all([ fs.writeFile(filePath, JSON.stringify(thread, null, 2)), fs.writeFile(txtPath, thread.serializeForLLM()) ]); return id; } async get(id: string): PromiseThread | undefined { const filePath path.join(this.threadsDir, ${id}.json); const data await fs.readFile(filePath, utf8).catch(() null); if (!data) return undefined; return new Thread(JSON.parse(data).events); } // update 同理同时写 json 与 txt }注意这里一个很有代表性的细节创建线程时同时写两份文件——${id}.json结构化、可反序列化回Thread和${id}.txtthread.serializeForLLM()生成的、直接喂给 LLM 的文本。这正体现了执行状态与业务状态统一为同一份数据同一份events一个 JSON 视角给程序一个文本视角给 LLM没有任何额外同步逻辑。3. 调试Debugging完整历史在一处可见The entire history is visible in one place——因为所有状态都在一条线程里调试时你只需要看一个文件/一个数据结构就能复盘 Agent 的每一步决策。沿用上面的FileSystemThreadStore每一条会话在.threads/目录下都是一个uuid.json里面是缩进的完整事件序列。无论 Agent 中间经过多少次工具调用、多少次人类介入历史都完整可查不需要去拼接执行日志 业务表 消息表三份数据。4. 灵活性Flexibility新增状态只需新增事件类型Easy to add new state by just adding new event types——因为状态由事件列表构成扩展状态就是扩展事件类型不需要改数据库 schema、不需要迁移。在模板里Event.type就是普通字符串见上文agent.ts的Event接口agentLoop中新增一种工具只需在switch里加一个分支并往thread.events里 push 一个新事件agent.tscase add: result nextStep.a nextStep.b; thread.events.push({ type: tool_response, data: result }); return thread; // subtract / multiply / divide 同理想要新增等待人类审批等待澄清这类执行状态同样只是新的事件类型例如request_more_information、request_approval_from_manager由Thread上的awaitingHumanResponse()/awaitingHumanApproval()这类纯函数来解读agent.ts——解读逻辑不依赖任何外部状态表。5. 恢复Recovery加载线程即可从任意点继续Can resume from any point by just loading the thread——这正是模板服务端outer loop的核心机制。看 packages/create-12-factor-agent/template/src/server.ts当收到人类响应或工具审批完成的回调时服务端只做一件事——从 body 里取出thread_id加载线程push 新事件再继续跑内循环case human_contact.completed: case function_call.completed: threadId body.event.spec.state?.thread_id; if (!threadId) { notFound(res); return; } thread store.get(threadId); // ... break;随后无论事件类型如何统一走push 事件 →innerLoop(thread)→store.update(threadId, newThread)的路径。恢复 重新加载一条线程没有任何额外的从第几步恢复的流程状态需要重建。在 CLI 路径 packages/create-12-factor-agent/template/src/cli.ts 中同理threadStore.create(thread)拿 idagentLoop(thread)之后threadStore.update(threadId, newThread)循环往复。6. 分叉Forking复制线程子集即可生成新会话Can fork the thread at any point by copying some subset of the thread into a new context / state ID——既然一切状态都是事件数组分叉就退化成一次数组拷贝。图 img/150-unify-state.png 中明确标注了 forking from previous context window is easy!想基于当前会话开一条平行分支例如换一种方式重试从某一步重新规划只需要取thread.events的一个前缀/子集构造一个新的Threadnew Thread(eventsSubset)并赋一个新的线程 ID 即可。这与 Factor 12Make your agent a stateless reducer 的思路一脉相承——状态是纯数据任何变换拼接、截断、分支都可以无副作用地进行。7. 人类界面与可观测性Human Interfaces and Observability线程可平移为 Markdown 或富 Web UITrivial to convert a thread into a human-readable markdown or a rich Web app UI——因为线程是结构化的、有序的事件列表渲染成人类可读的界面只是遍历打印的问题。模板的 agent.ts 中serializeForLLM()就是最好的例子它把每个事件渲染成 XML 风格的块intent.../intent天然就是人类可读的文本格式serializeForLLM() { return this.events.map(e this.serializeOneEvent(e)).join(\n); } serializeOneEvent(e: Event) { return this.trimLeadingWhitespace( ${e.data?.intent || e.type} ${ typeof e.data ! object ? e.data : Object.keys(e.data).filter(k k ! intent).map(k ${k}: ${e.data[k]}).join(\n)} /${e.data?.intent || e.type} ) }这份文本既是 LLM 的输入也是运维/用户可以直接阅读的聊天记录——FileSystemThreadStore同步写入的${id}.txt就是这条可观测通道的落盘版本。对同一个数据源做不同的渲染喂模型、给人看、渲染 Web UI正是统一状态的直接红利。五、仓库实战从原理到可运行代码5.1 内循环inner loop状态只在事件上累积模板的 agent.ts 完整展示了统一状态下的 Agent 主循环export async function agentLoop(thread: Thread): PromiseThread { while (true) { const nextStep await b.DetermineNextStep(thread.serializeForLLM()); thread.events.push({ type: tool_call, data: nextStep }); switch (nextStep.intent) { case done_for_now: case request_more_information: case request_approval_from_manager: return thread; // 需要人类介入退出内循环 case divide: return thread; // 危险操作交回给外层做审批 case add: case subtract: case multiply: thread await handleNextStep(nextStep, thread); // 执行工具结果 append 回 events } } }注意循环每一步都在向同一个thread.events追加事件——LLM 的决策是事件、工具的结果是事件、终止原因也是事件。循环退出时执行状态lastEvent().data.intent与业务状态全部事件存在于同一个对象里没有第二条状态路径。5.2 状态存储接口换实现不换理念packages/create-12-factor-agent/template/src/state.ts 定义了极简的存储接口export interface ThreadStore { create(thread: Thread): Promisestring; get(id: string): PromiseThread | undefined; update(id: string, thread: Thread): Promisevoid; }文件里有一句非常关键的注释you can replace this with any simple state management, e.g. redis, sqlite, postgres, etc也就是说理念是线程即状态而承载线程的存储可以是文件、内存、Redis、SQLite 或 Postgres——替换存储不会破坏统一状态这一设计因为状态本身只有一种形态线程。workshop 09workshops/2025-05/sections/09-state-management/README.md给出了一个更极简的内存版实现作为对照export class ThreadStore { private threads: Mapstring, Thread new Map(); create(thread: Thread): string { const id crypto.randomUUID(); this.threads.set(id, thread); return id; } get(id: string): Thread | undefined { return this.threads.get(id); } update(id: string, thread: Thread): void { this.threads.set(id, thread); } }把Mapstring, Thread换成 Redis / SQLite就是生产级的同构实现。workshop 09 还演示了配套的 API 演进POST /thread返回thread_id与response_urlGET /thread/:id读取线程POST /thread/:id/response把人类回复 push 进线程后重新跑agentLoop——恢复与澄清全部建立在加载线程、追加事件之上。5.3 外循环outer loopthread_id 是唯一的恢复凭据server.ts 中的outerLoop展示了统一状态的完整生命周期conversation.created→ 用用户消息构造新线程new Thread([{type: conversation.created, data: body.event.user_message}])内循环退出需要人类介入→ 把thread_id塞进人类联系/工具审批请求的spec.state里一起发出人类响应或审批回调回来 → 凭thread_idstore.get(threadId)恢复线程 → 追加human_response或tool_response事件 → 再次innerLoop→store.update。整个过程里这个会话进行到哪了从未被单独存储过它要么体现在thread.events的最后一个事件里要么被序列化后存在存储中。thread_id只是一个指向整份状态的指针不是一份独立状态。六、与相邻 Factor 的关系统一执行状态与业务状态并不是孤立原则原文档与仓库中多个章节相互呼应与 Factor 3Own your context window 的关系统一状态的前提是执行状态可以从上下文窗口推断而掌控上下文窗口自定义上下文格式、控制送入 LLM 的内容正是 Factor 3 的主题。原文档明确指出通过 Factor 3 你才能控制实际进入 LLM 的内容从而放心地把状态全部放进上下文。Factor 3 中Thread/Event、thread_to_prompt的示例代码与本文serializeForLLM()的实现思路完全同源。与 Factor 6Launch/Pause/Resume with simple APIs 的关系暂停/恢复之所以能用简单 API实现正是因为状态统一——恢复就是加载线程。Factor 6 文档明确标注其与 Factor 5密切相关但可以独立实现。注意 Factor 6 还点出一个常见陷阱很多 AI 编排器虽然支持暂停/恢复却不支持工具选择与工具执行之间的暂停——这正是统一状态下由线程即状态天然解决的空档。与 Factor 12Make your agent a stateless reducer 的关系Factor 12 把 Agent 抽象为无状态 reducer输入旧状态事件输出新状态Factor 5 的线程即状态正是这个抽象在工程层面的落地——agentLoop(thread) - Thread本质上就是一个 reducer而ThreadStore只是这个 reducer 状态的持久化层。七、工程建议与常见误区综合原文档观点与模板源码给出几条落地建议默认走统一路线除非你有确凿的、超出上下文窗口能力的状态需求如跨会话共享的凭据、需要独立审计的流程表否则不要主动引入执行状态表 业务事件流的双轨设计——原文档提醒这很可能是过度设计。把例外最小化session id、密码上下文这类放不进上下文窗口的东西应当收敛为少数线程级元数据而不是演变成一套并行状态机。用事件类型表达等待/重试/终止等待人类响应、等待审批、需要重试都可以建模为特定类型的事件模板中的request_more_information、request_approval_from_manager、done_for_now即为此而生解读逻辑写成Thread上的纯函数。存储层可以随意替换文件、内存、Redis、Postgres 都只是ThreadStore接口的一个实现替换存储不应改变上层加载线程→追加事件→继续循环的调用方式。警惕双份状态漂移一旦引入独立的执行状态记录最常见的故障就是执行记录与事件历史不一致例如表里写着等待审批而线程最后一个事件其实是工具结果。统一状态从根源上消除了这一类 bug。结语Factor 5 用一句话概括就是你的 Agent 不需要两本账——把执行状态当作业务状态的投影让一条线程承载一切。从仓库模板的Thread类、FileSystemThreadStore、agentLoop到 workshop 09 的内存版ThreadStore都验证了这套设计的可操作性状态简单到新增一种事件就是新增一种状态恢复简单到加载一条线程分叉简单到拷贝一段数组。如果你也在为 Agent 的进度管理感到头疼不妨先砍掉那套独立的执行状态表看看你的上下文窗口是不是已经包含了答案。赞分享文档教程人工智能大模型AI Agent【免费下载链接】12-factor-agentsWhat are the principles we can use to build LLM-powered software that is actually good enough to put in the hands of production customers?项目地址https://gitcode.com/GitHub_Trending/12/12-factor-agents点击查看免费下载相关推荐12-Factor Agents状态管理统一执行状态与业务状态12 Factor Agents状态管理统一执行状态与业务状态 你是否曾经在构建AI应用时陷入状态管理的泥潭执行状态、业务状态、会话状态、任务状态...各种文档教程人工智能大模型AI Agent12-Factor Agents 之 Factor 5统一执行状态与业务状态让 Agent 状态只有一种真相12 Factor Agents 之 Factor 5统一执行状态与业务状态让 Agent 状态只有一种真相 导读 本文是 12 Factor Agents文档教程人工智能大模型AI AgentAwesome Scala状态管理ZIO State与Cats State MonadAwesome Scala状态管理ZIO State与Cats State Monad 在Scala应用开发中状态管理是处理可变数据和复杂业务逻辑的关键环节文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →