首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Agent 框架设计:从选型判断到最小状态机实现

Agent 框架设计:从选型判断到最小状态机实现

原创
作者头像
搜weiranit.fun
发布于 2026-11-13 18:06:45
发布于 2026-11-13 18:06:45
370
举报

一、选型的真正变量:状态复杂度与退出成本

一个被反复验证的选型原则是:先问“需要确定性还是灵活性”,再问“在哪个生态”,最后看多 Agent 的复杂度。但这个原则需要被翻译成更具体的工程判断。

状态管理是分水岭。LangChain 的线性链式执行在固定流程中足够用,但一旦任务需要分支、循环、或中断恢复,它的缺陷就暴露了:无统一全局状态,上下文依靠各组件单独传递,长流程中极易出现数据断层。LangGraph 把应用建模为有向状态图,节点处理逻辑、边控制流转、状态在整个图中以受控方式流动,每个节点返回增量更新而非替换整个状态,框架负责原子性合并。这套机制支撑了断点续跑、时间旅行调试和人工介入,代价是学习曲线和前期设计成本。

替换成本决定选型纪律。换一个 MCP 服务器改一行配置就够,换编排层则需要重写状态 schema、节点和边。这意味着编排层的选择应该最谨慎,也最应该基于约束而非偏好。如果你的场景是“顺序调用三个工具然后返回结果”,LangGraph 的图 schema、节点定义、编译流程就显得过重;但如果任务可能持续数小时、需要在崩溃后从上一个节点恢复、或者需要人工审批节点,那些前期成本就是必要的保险。

一个容易被忽略的判断维度是模型锁定风险。OpenAI Agents SDK 的 Handoff 机制设计简洁,但它是线性链而非图拓扑,无内置状态持久化。如果你的 Agent 需要在一年内保持模型无关性,LangGraph 和 Pydantic AI 的摩擦最小;如果深度绑定 OpenAI 生态可以接受,前者的开发速度确实更快。

二、框架必须解决的三个设计问题

抛开具体框架的 API 差异,一个能支撑生产 Agent 的运行时,底层必须处理好三件事。

第一,状态的可变性边界。Agent 的每一轮执行都在产生新信息:模型返回的工具调用请求、工具执行的结果、可能的错误信息。这些信息需要被追加到上下文中,否则模型下一轮就“忘记”了自己刚做了什么。但状态不应该被节点直接修改。LangGraph 的设计是:节点拿到的是只读快照,返回的是增量更新,框架负责合并。这个约束看似繁琐,但它保证了并行节点不会互相覆盖、崩溃后可以从检查点精确恢复。没有这层约束的框架,在单线程简单场景下看不出问题,一旦引入并发或需要回滚,状态不一致就会成为难以调试的噩梦。

第二,循环的终止条件。Agent 不是靠“聪明”停下来的。一个没有硬性边界的 Agent 循环,可能因为模型反复请求同一个工具、或者陷入两个 Agent 之间的交接死循环而无限运行。OpenAI Agents SDK 的 Handoff 设计有一个已知风险:Agent A 交给 Agent B,B 又交回 A,形成循环。缓解方案是增加 hop counter。LangGraph 的递归上限则在编译时设置,运行时到达上限即终止。无论框架是否内置,设计者必须在自己的循环逻辑中显式定义终止条件:最大步数、模型输出 done 信号、外部超时,三者至少取其一。

第三,工具调用的安全边界。Agent 框架与普通 LLM 调用的关键区别在于工具执行。OpenAI Agents SDK 把 Guardrail 分为输入、输出、工具调用三层,工具级 Guardrail 在函数执行前校验参数、检查权限。这层设计的意义在于:模型可能产生看似合理但实际危险的参数。如果工具涉及写操作、删除、转账或调用外部 API,没有这层校验就是在裸奔。即使不使用框架的内置 Guardrail,自研循环中也必须在路由层做参数校验和副作用控制。

三、一段不依赖框架的最小状态机实现

以下代码实现一个带持久化检查点和递归上限的 Agent 循环。它不使用 LangGraph 或任何 Agent 框架,目的是让状态流转、工具路由和终止逻辑完全透明。运行需要设置 DASHSCOPE_API_KEY 或替换为任意兼容 OpenAI 接口的配置。

代码语言:javascript
复制
import json, os, sqlite3
from openai import OpenAI

client = OpenAI(
    api_key=os.getenv('DASHSCOPE_API_KEY'),
    base_url='https://dashscope.aliyuncs.com/compatible-mode/v1'
)

# ---- 检查点存储(最小实现:SQLite 持久化)----
class CheckpointStore:
    def __init__(self, db_path='agent_checkpoints.db'):
        self.conn = sqlite3.connect(db_path, check_same_thread=False)
        self.conn.execute(
            'CREATE TABLE IF NOT EXISTS cp (thread_id TEXT PRIMARY KEY, state TEXT)'
        )
        self.conn.commit()

    def save(self, thread_id, state):
        self.conn.execute(
            'INSERT OR REPLACE INTO cp VALUES (?, ?)',
            (thread_id, json.dumps(state))
        )
        self.conn.commit()

    def load(self, thread_id):
        row = self.conn.execute(
            'SELECT state FROM cp WHERE thread_id=?', (thread_id,)
        ).fetchone()
        return json.loads(row[0]) if row else None

# ---- 工具定义 ----
def get_time() -> str:
    import datetime
    return datetime.datetime.now().isoformat()

def add(a: int, b: int) -> str:
    return str(a + b)

TOOL_MAP = {'get_time': get_time, 'add': add}

tools_schema = [
    {"type": "function", "function": {
        "name": "get_time", "description": "获取当前时间",
        "parameters": {"type": "object", "properties": {}}
    }},
    {"type": "function", "function": {
        "name": "add", "description": "两数相加",
        "parameters": {
            "type": "object",
            "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
            "required": ["a", "b"]
        }
    }}
]

# ---- 状态机循环 ----
def run_agent(thread_id: str, user_input: str, max_steps: int = 8):
    store = CheckpointStore()
    state = store.load(thread_id) or {"messages": [], "step": 0}
    state["messages"].append({"role": "user", "content": user_input})

    while state["step"] < max_steps:
        resp = client.chat.completions.create(
            model="qwen-plus",
            messages=state["messages"],
            tools=tools_schema
        )
        msg = resp.choices[0].message

        # 终止条件:模型不再请求工具,直接返回文本
        if not msg.tool_calls:
            state["messages"].append({"role": "assistant", "content": msg.content})
            store.save(thread_id, state)
            return msg.content

        # 将模型决策追加到状态(增量更新)
        state["messages"].append(msg)

        # 工具路由与执行
        for tc in msg.tool_calls:
            name = tc.function.name
            args = json.loads(tc.function.arguments)
            if name not in TOOL_MAP:
                result = f"未知工具: {name}"
            else:
                try:
                    result = TOOL_MAP[name](**args)
                except Exception as e:
                    result = f"工具执行错误: {e}"

            state["messages"].append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": str(result)
            })

        state["step"] += 1
        store.save(thread_id, state)  # 每轮落盘

    return "达到最大步数限制,任务未完成。"

# 使用:同一 thread_id 再次调用会从上次状态继续
# print(run_agent("session-1", "现在几点?然后算 3+7"))

这段代码的三个设计点对应了上一节讨论的三个问题。state["step"] 是递归上限的硬性保障,每轮递增,到达 max_steps 即终止。store.save 在每轮工具执行后落盘,如果进程崩溃,用同一个 thread_id 再次调用会从上次的消息列表继续,而不是从头开始。工具执行被包裹在 try/except 中,错误以 tool 消息的形式返回给模型,而不是让整个循环崩溃——模型可以在下一轮看到“工具执行错误”并尝试调整参数或换一个工具。

这段代码故意没有实现的东西同样值得注意:没有并行工具调用(多个 tool_calls 是顺序执行的)、没有人工审批节点、没有多 Agent 交接。这些能力在生产中可能需要,但它们的正确实现依赖于前面三个基础设计的稳固。在把状态管理和终止逻辑做扎实之前,叠加更多能力只会让调试难度指数级上升。

四、选型之后真正重要的事

框架选型有一个被反复强调但很少被执行的建议:可观测性从第一天开始。LangGraph 配 LangSmith 提供了成熟的追踪方案,OpenAI Agents SDK 内置 Tracing。如果使用自研循环,至少要把每一轮的 messages 完整记录,否则 Agent 出错时你无法分辨是模型选错了工具、参数传错了、还是工具本身返回了异常。

另一个在 2026 年被正式化的设计原则来自企业 AgenticOps 参考架构:确定性优先于生成。在把任何环节交给模型决策之前,先问这个环节是否可以用确定性代码解决。格式化输出、参数校验、权限检查、状态流转的路由判断,这些都不需要模型参与。把它们从 Agent 的“自主决策”范围中剥离出去,既降低了成本,也降低了不可预测性。框架的价值在于让模型在真正需要判断的地方发挥作用,而不是把整个系统变成一个不可调试的概率机器。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、选型的真正变量:状态复杂度与退出成本
  • 二、框架必须解决的三个设计问题
  • 三、一段不依赖框架的最小状态机实现
  • 四、选型之后真正重要的事
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档