Agent ≠ 魔法,告别框架黑箱,拥抱状态机
你是否也走过这样一条路?
- 你想做一个 Agent,解决一个具体问题。
- 你兴奋地规划产品,设计交互,满怀期待。
- 为了"快",你一把抄起市面上最火的 Agent 框架,
pip install
,然后闷头"构建"。 - 你很快做出了一个看起来还不错的 Demo,达到了 70-80 分。
- 但当你尝试把产品推向真实用户时,才发现这 80 分远远不够。那剩下的 20 分,像一道天堑。
- 为了跨过这道天堑,你开始疯狂调试,逆向工程框架的 prompt、深入它复杂的调用链,试图搞懂它到底在"黑箱"里做了什么。
- 最后,在某个深夜,你精疲力尽,决定从头再来。
我已经对那些动辄上万行代码的框架失去了兴趣。本文试着基于软件的本质--状态机,构建一个极简但是有效的Agent架构。
LLM 是无状态函数
让我们先放下所有复杂的框架和概念,回到原点,凝视我们的核心"组件"------那个大语言模型(LLM)。
它到底是什么?
LLM 是无状态函数。 LLMs are stateless functions
什么意思?它就像一个记忆力只有七秒的、拥有超级大脑的天才。你每次调用它,都是一次全新的开始。它不记得你上一秒说了什么。我们之所以感觉它有记忆,是因为我们调用LLM API时候,将整个聊天历史又重新发给了它一遍。
如果 LLM 本身是无状态的,那么状态(State)就必须由我们自己来管理 。如果 LLM 的所有决策都只依赖于它在调用瞬间所看到的"世界",那么构建这个世界(Context Engineering)就成了我们唯一能做的事情。
Context is everything
上下文,就是你唯一能够撬动 LLM 能力的那个支点。我们所有的工程努力,都应该围绕着如何为 LLM 精心构建每一次交互的上下文展开。
一个常规的context包含下面部分:
指令:你要它扮演什么角色,遵守什么规则,目标任务是什么。常规prompt。
外部信息 :背景信息或者要求LLM参考的信息
工具调用:告诉它哪些外部工具可以使用,以及工具使用的结果
相关记忆 (Memory):之前聊天的对话历史(短期记忆)。用户的长期偏好、要求被记住的事实和观点等(长期记忆)
输出要求:限制输出范围,例如输出的格式要求、字数要求、语言风格等
context是LLM看到的全部世界。context是否清晰、准确、信息充沛决定了LLM返回的质量。Garbage in, garbage out.
在你觉得AI反馈不好的时候,你需要先考虑在当前上下文情况下,AI是否有可能完成任务?
这里还有一些我在实践中发现的小技巧:
其一,用 XML/YAML 代替 JSON,跟模型说"人话"。
我们工程师很喜欢 JSON,但 LLM 未必。那些大括号、引号、逗号对它来说,其实是一种很"重"的认知负担。
我发现,在很多场景下,用更接近自然语言的XML格式,效果出奇地好。
xml
<function_call>
<tool_name>check_availability</tool_name>
<parameters>
<room>观星阁</room>
<time>15:00</time>
</parameters>
</function_call>
为什么这样更好?
- 对注意力机制更友好 (Attention-Friendly): 清晰的 XML 标签就像文章的标题,能天然地引导 LLM 的注意力,让它更容易抓住关键信息。(注意力引导非常重要,attention是LLM的底层机制)
- 容错性更高: 相比 JSON 那种"少一个逗号就全盘崩溃"的严格语法,这类格式的容错性要好得多。
- Token 更省: 通常比带有很多引号和括号的 JSON 更紧凑。
其二,打破 user/assistant/tool
的枷锁 别被传统的 user/assistant/tool
消息格式束缚。 上下文窗口是 LLM 的全部世界,你怎么"排版"这个世界,至关重要。不要让LLM在一堆散乱的 user/assistant
消息里费力地寻找线索。 你可以大胆地用 XML 标签、YAML、或者任何你自定义的格式来组织对话记录、工具调用,让信息密度更高,对模型更友好。你也不需要局限在OpenAI提供的标准function call格式里面,你可以参考Claude的system prompt的设计,很轻松实现更为简约有效的function call机制。
总之,核心要旨是:组织一个对LLM来说,更清晰、明确、容易理解的上下文。
状态机
软件的本质是状态机 。程序运行,是从一个确定的状态,通过一个明确的动作,迁移到下一个状态的过程。
这个软件哲学,在 Agent 时代,依然是我们手中最可靠的工具。 我们可以用它来清晰地定义一个 Agent 的核心循环:
当前状态 (State) -> 上下文构建 (Context) -> LLM 决策 (LLM) -> 动作执行 (Action) -> 新状态 (New State)
我们来分解这个循环的每一个阶段:
-
State (状态): 系统的状态,由一个事件列表 (Event Stream) 来表示。这是一个简洁的列表,记录了从交互开始到现在的所有事件,例如用户的请求、工具的调用结果、Agent 自己的思考......
State
就是所有这些事件的集合。 -
Context (上下文构建): 在调用 LLM 之前,一个
ContextBuilder
函数会介入。它的唯一职责是:读取当前的State
(事件列表),构建一个完善的上下文,作为 LLM 的输入。这份上下文包含了指令、相关历史、可用的工具、输出格式要求等。这是我们唯一可以控制 LLM 输入的地方。 -
LLM (决策): 我们将精心构建的
Context
提交给 LLM。LLM 的任务不是直接生成最终回复,而是决策出下一步,比如"调用某个工具"或"回复用户"。 -
Action (执行): 我们的代码拿到LLM的反馈,执行相应的、确定性的逻辑。调用 API、查询数据库、返回消息给用户......这些都是我们 100% 可控的。
-
New State (状态更新):
Action
执行后会产生一个结果。这个结果被包装成一个新的"事件"对象,并追加到事件列表的末尾。这样,系统就从State
迁移到了New State
。
在这个架构里,LLM 的角色被严格限定在其最擅长的领域:基于上下文进行推理和决策。而系统的其他部分------状态管理、业务逻辑执行------则完全由我们自己的、可预测的代码来控制。
传统的编程,状态转移的逻辑是我们手写的;而在这个模型里,状态转移的决策逻辑,被外包给了 LLM。 这是最核心的变化。
模块化扩展:工具与记忆
当你拥有了上面那个坚实的状态机"底盘"后,扩展 Agent 的能力就变得非常清晰。
需要 Agent 调用外部 API?那就为它设计一个"工具 (Tool)"模块。
需要 Agent 记住长期的对话上下文?那就为它实现一个"记忆 (Memory)"模块。
这两个模块,不再是独立于系统之外的黑箱,而是这个状态机架构下的标准化组件。
-
工具箱的设计与实现:它需要一套标准的接口,能让 Agent 在"上下文构建"阶段,向 LLM 声明其能力。它的调用和返回结果,都以标准化的"事件"形式,融入到我们的主循环中。
- 更多关于工具系统的详细设计,请参考附文:《为 Agent 打造一个可演化的工具箱》。
-
记忆模块的实现 :它负责在"上下文构建"阶段,根据当前情境,从完整的历史记录中提取最相关的部分。一个好的记忆模块,可以借鉴生物大脑的设计,将其划分为处理当前对话的短期记忆 ,和存储核心事实与用户偏好的长期记忆
- 关于如何设计一个不只是存储历史,而是能主动优化和总结的记忆系统,请参考附文:《Agent的记忆不是数据库,而是认知模型》。
尾声:凝视未来
我们今天所讨论的这套架构,它的哲学是清晰的:人驾驭 AI。
我们用我们最熟悉的、确定性的软件工程框架,为 LLM 这个强大的不确定性引擎,套上了一个可靠的"缰绳",划定了一个清晰的"围栏"。方向盘和刹车,始终牢牢掌握在我们自己手里。
这是一种务实且安全的路径。但,它会是唯一的终局吗?
我们可以预见,还存在着另一条截然不同的路径:AI 作为平台。
在这条路径上,当未来的模型足够强大和可靠时,我们可能不再需要自己去维护那个精巧的状态机。LLM 内核本身就拥有了管理自身状态、规划复杂任务、甚至自我认知优化的能力。
那时,我们工程师的角色,可能会从今天的"系统建筑师",转变为纯粹的"工具匠"。我们的核心工作,不再是编排复杂的业务逻辑,而是为这个 AI 平台,打造一套精良的、原子化的、高可用的工具集(APIs)。我们提供工具和说明书,AI 自己决定何时、何地、如何使用它们来达成我们设定的高层目标。
那将是一个彻底解放生产力的世界,也是"AI失控"最不确定的时代。只是回到今天,构建一个自己的Agent没那么困难,希望本文能给你一个参考。
参考文献:
状态机的思维学习自JYY老师《操作系统》课程 (感谢JYY老师的启蒙)
本文GitHub仓库:simple_agent
微信公众号 秋水东行