AI Agent 十二要素方法论:构建生产级智能体的完整指南

本文是对 12 Factor Agents 项目的深度解读与翻译总结,旨在帮助开发者理解并构建可靠、可维护的 AI Agent 系统。

目录

  • 引言:我们是如何走到这一步的
  • [什么是真正的 Agent](#什么是真正的 Agent "#%E4%BB%80%E4%B9%88%E6%98%AF%E7%9C%9F%E6%AD%A3%E7%9A%84-agent")
  • 十二要素详解
    • [要素 1:自然语言到工具调用](#要素 1:自然语言到工具调用 "#%E8%A6%81%E7%B4%A0-1%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5%88%B0%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8")
    • [要素 2:掌控提示词](#要素 2:掌控提示词 "#%E8%A6%81%E7%B4%A0-2%E6%8E%8C%E6%8E%A7%E6%8F%90%E7%A4%BA%E8%AF%8D")
    • [要素 3:掌控上下文窗口](#要素 3:掌控上下文窗口 "#%E8%A6%81%E7%B4%A0-3%E6%8E%8C%E6%8E%A7%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AA%97%E5%8F%A3")
    • [要素 4:工具只是结构化输出](#要素 4:工具只是结构化输出 "#%E8%A6%81%E7%B4%A0-4%E5%B7%A5%E5%85%B7%E5%8F%AA%E6%98%AF%E7%BB%93%E6%9E%84%E5%8C%96%E8%BE%93%E5%87%BA")
    • [要素 5:统一执行状态与业务状态](#要素 5:统一执行状态与业务状态 "#%E8%A6%81%E7%B4%A0-5%E7%BB%9F%E4%B8%80%E6%89%A7%E8%A1%8C%E7%8A%B6%E6%80%81%E4%B8%8E%E4%B8%9A%E5%8A%A1%E7%8A%B6%E6%80%81")
    • [要素 6:启动/暂停/恢复的简单 API](#要素 6:启动/暂停/恢复的简单 API "#%E8%A6%81%E7%B4%A0-6%E5%90%AF%E5%8A%A8%E6%9A%82%E5%81%9C%E6%81%A2%E5%A4%8D%E7%9A%84%E7%AE%80%E5%8D%95-api")
    • [要素 7:用工具调用联系人类](#要素 7:用工具调用联系人类 "#%E8%A6%81%E7%B4%A0-7%E7%94%A8%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8%E8%81%94%E7%B3%BB%E4%BA%BA%E7%B1%BB")
    • [要素 8:掌控控制流](#要素 8:掌控控制流 "#%E8%A6%81%E7%B4%A0-8%E6%8E%8C%E6%8E%A7%E6%8E%A7%E5%88%B6%E6%B5%81")
    • [要素 9:将错误压缩进上下文窗口](#要素 9:将错误压缩进上下文窗口 "#%E8%A6%81%E7%B4%A0-9%E5%B0%86%E9%94%99%E8%AF%AF%E5%8E%8B%E7%BC%A9%E8%BF%9B%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AA%97%E5%8F%A3")
    • [要素 10:小而专注的 Agent](#要素 10:小而专注的 Agent "#%E8%A6%81%E7%B4%A0-10%E5%B0%8F%E8%80%8C%E4%B8%93%E6%B3%A8%E7%9A%84-agent")
    • [要素 11:从任何地方触发,在用户所在的地方服务](#要素 11:从任何地方触发,在用户所在的地方服务 "#%E8%A6%81%E7%B4%A0-11%E4%BB%8E%E4%BB%BB%E4%BD%95%E5%9C%B0%E6%96%B9%E8%A7%A6%E5%8F%91%E5%9C%A8%E7%94%A8%E6%88%B7%E6%89%80%E5%9C%A8%E7%9A%84%E5%9C%B0%E6%96%B9%E6%9C%8D%E5%8A%A1")
    • [要素 12:让 Agent 成为无状态 Reducer](#要素 12:让 Agent 成为无状态 Reducer "#%E8%A6%81%E7%B4%A0-12%E8%AE%A9-agent-%E6%88%90%E4%B8%BA%E6%97%A0%E7%8A%B6%E6%80%81-reducer")
  • 附录:预取上下文
  • 总结

引言:我们是如何走到这一步的

无论你是 Agent 领域的新手还是资深老手,本文都将试图说服你抛开对 AI Agent 的固有认知,从第一性原理重新思考它们。

软件发展简史与 Agent 的演进

60 年前:软件即有向图

我们经常会谈到有向图(DG)和它们的无环版本------有向无环图(DAG)。值得一提的是,软件本身就是一个有向图。这就是为什么我们过去常用流程图来表示程序。

markdown 复制代码
开始 → 输入 → 处理 → 判断 → 输出 → 结束
              ↑          ↓
              ← 循环处理 ←

20 年前:DAG 编排器兴起

大约 20 年前,DAG 编排器开始流行起来。经典的如 AirflowPrefect,以及更新的 DagsterInngestWindmill 等。它们遵循相同的图模式,额外提供了可观测性、模块化、重试、管理等功能。

10-15 年前:ML 模型融入 DAG

当机器学习模型开始变得足够强大可用时,我们开始看到带有 ML 模型的 DAG。你可以想象这样的步骤:"将此列中的文本摘要到新列"或"按严重程度或情感对支持问题进行分类"。

但归根结底,它仍然主要是相同的确定性软件。

Agent 的承诺

学习 Agent 时最大的收获是:你可以抛弃 DAG。不再需要软件工程师编写每个步骤和边缘情况,你只需给 Agent 一个目标和一组转换,让 LLM 实时决策找出路径。

这里的承诺是:

  • 写更少的代码
  • 只需给 LLM 图的"边",让它自己找出节点
  • 可以从错误中恢复
  • LLM 可能会找到解决问题的新颖方案

Agent 即循环

换句话说,你有一个由以下步骤组成的循环:

  1. LLM 确定工作流的下一步,输出结构化 JSON("工具调用")
  2. 确定性代码执行工具调用
  3. 结果附加到上下文窗口
  4. 重复直到下一步被确定为"完成"
python 复制代码
initial_event = {"message": "..."}
context = [initial_event]

while True:
    next_step = await llm.determine_next_step(context)
    context.append(next_step)

    if next_step.intent == "done":
        return next_step.final_answer

    result = await execute_step(next_step)
    context.append(result)

"循环直到解决"模式的问题

这种模式最大的问题是:

当上下文窗口过长时,Agent 会迷失方向------它们会不断尝试相同的失败方法

即使你没有手工构建过 Agent,你可能也在使用 Agentic 编码工具时遇到过这个长上下文问题。它们在一段时间后就会迷失,你需要开始新的对话。

我甚至会提出一个你可能已经发展出自己直觉的观点:

即使模型支持越来越长的上下文窗口,小而专注的提示和上下文总是能得到更好的结果

大多数构建者在意识到超过 10-20 轮的任何事情都会变成 LLM 无法恢复的大混乱后,就把"工具调用循环"的想法放到一边了。即使 Agent 90% 的时间都能做对,那离"足够好到可以交给客户"还差得很远。你能想象一个 10% 页面加载都会崩溃的 Web 应用吗?

真正有效的:微型 Agent

我在实际中看到很多的一件事是将 Agent 模式散布到更广泛的确定性 DAG 中。

通过让语言模型管理范围良好的任务集,可以轻松地整合实时的人类反馈,将其转化为工作流步骤,而不会陷入上下文错误循环。

真实的微型 Agent 示例

以下是一个确定性代码如何运行一个负责处理部署的人机交互步骤的微型 Agent 示例:

  1. 人类 将 PR 合并到 GitHub main 分支
  2. 确定性代码 部署到 staging 环境
  3. 确定性代码 对 staging 运行端到端测试
  4. 确定性代码 将任务交给 Agent 进行生产部署,初始上下文:"将 SHA 4af9ec0 部署到生产环境"
  5. Agent 调用 deploy_frontend_to_prod(4af9ec0)
  6. 确定性代码 请求人类批准此操作
  7. 人类 拒绝操作并反馈"你能先部署后端吗?"
  8. Agent 调用 deploy_backend_to_prod(4af9ec0)
  9. 确定性代码 请求人类批准此操作
  10. 人类 批准操作
  11. 确定性代码 执行后端部署
  12. Agent 调用 deploy_frontend_to_prod(4af9ec0)
  13. 确定性代码 请求人类批准此操作
  14. 人类 批准操作
  15. 确定性代码 执行前端部署
  16. Agent 确定任务成功完成,完成!
  17. 确定性代码 对生产环境运行端到端测试
  18. 确定性代码 任务完成,或传递给回滚 Agent 审查失败并可能回滚

什么是真正的 Agent

一个 Agent 本质上由以下四个组件构成:

  1. 提示词(Prompt) - 告诉 LLM 如何行为,以及它有哪些可用的"工具"。提示的输出是描述工作流下一步的 JSON 对象("工具调用"或"函数调用")
  2. Switch 语句 - 根据 LLM 返回的 JSON,决定如何处理它
  3. 累积的上下文 - 存储已发生步骤及其结果的列表
  4. For 循环 - 直到 LLM 发出某种"终止"工具调用(或纯文本响应),将 switch 语句的结果添加到上下文窗口并要求 LLM 选择下一步

在"部署机器人"示例中,拥有控制流和上下文累积带来几个好处:

  • 在我们的 switch 语句for 循环 中,我们可以劫持控制流来暂停等待人类输入或等待长时间运行任务的完成
  • 我们可以轻松地序列化 上下文 窗口以实现暂停+恢复
  • 在我们的 提示 中,我们可以优化如何向 LLM 传递指令和"到目前为止发生了什么"

十二要素详解

要素 1:自然语言到工具调用

Agent 构建中最常见的模式之一是将自然语言转换为结构化工具调用。这是一个强大的模式,允许你构建能够推理任务并执行它们的 Agent。

这个模式的原子应用是将这样的短语:

"你能为 Terri 创建一个 750 美元的付款链接,用于赞助二月份的 AI Tinkerers 聚会吗?"

转换为描述 Stripe API 调用的结构化对象:

json 复制代码
{
  "function": {
    "name": "create_payment_link",
    "parameters": {
      "amount": 750,
      "customer": "cust_128934ddasf9",
      "product": "prod_8675309",
      "price": "prc_09874329fds",
      "quantity": 1,
      "memo": "二月份 AI tinkerers 聚会的付款链接"
    }
  }
}

从那里,确定性代码可以接收有效负载并处理它:

python 复制代码
# LLM 接收自然语言并返回结构化对象
next_step = await llm.determine_next_step(
    """
    为 Jeff 创建一个 750 美元的付款链接
    用于赞助二月份的 AI tinkerers 聚会
    """
)

# 根据其函数处理结构化输出
if next_step.function == 'create_payment_link':
    stripe.paymentlinks.create(next_step.parameters)
    return  # 或者你想要的任何东西
elif next_step.function == 'something_else':
    # ... 更多情况
    pass
else:  # 模型没有调用我们知道的工具
    # 做其他事情
    pass

核心价值:LLM 负责理解意图和提取参数,确定性代码负责安全可靠地执行操作。这种分离创建了清晰的职责边界。


要素 2:掌控提示词

不要将提示工程外包给框架。

这绝非新鲜建议。很多专家都强调过这一点:

"如果你正在使用 LangChain 且无法解释你正在做什么,你不知道自己在做什么。如果你正在抽象出提示,你做错了。"

一些框架提供了这样的"黑盒"方法:

python 复制代码
agent = Agent(
    role="...",
    goal="...",
    personality="...",
    tools=[tool1, tool2, tool3]
)

task = Task(
    instructions="...",
    expected_output=OutputModel
)

result = agent.run(task)

这对于开始使用一流的提示工程来说很棒,但通常很难调整和/或逆向工程以获得正确的 token 进入模型。

取而代之,拥有提示并将它们视为一等代码:

rust 复制代码
function DetermineNextStep(thread: string) -> DoneForNow | ListGitTags | DeployBackend | DeployFrontend | RequestMoreInformation {
  prompt #"
    {{ _.role("system") }}

    你是一个有帮助的助手,管理前端和后端系统的部署。
    你努力工作以确保通过遵循最佳实践和正确的部署程序来实现安全和成功的部署。

    在部署任何系统之前,你应该检查:
    - 部署环境(staging vs production)
    - 要部署的正确标签/版本
    - 当前系统状态

    你可以使用 deploy_backend、deploy_frontend 和 check_deployment_status 等工具来管理部署。
    对于敏感部署,使用 request_approval 获取人工验证。

    总是先思考要做什么,比如:
    - 检查当前部署状态
    - 验证部署标签存在
    - 如果需要请求批准
    - 在生产之前部署到 staging
    - 监控部署进度

    {{ _.role("user") }}

    {{ thread }}

    下一步应该是什么?
  "#
}

掌控提示的关键好处

  1. 完全控制:编写 Agent 需要的确切指令,没有黑盒抽象
  2. 测试和评估:为提示构建测试和评估,就像对任何其他代码一样
  3. 迭代:根据真实世界的性能快速修改提示
  4. 透明度:确切知道 Agent 正在使用什么指令
  5. 角色黑客:利用支持非标准用户/助手角色用法的 API------例如,现已弃用的非聊天风格的 OpenAI "completions" API。这包括一些所谓的"模型洗脑"技术

记住:提示是应用逻辑和 LLM 之间的主要接口。我不知道什么是最好的提示,但我知道你希望有灵活性来尝试一切。


要素 3:掌控上下文窗口

你不一定需要使用标准的基于消息的格式来向 LLM 传达上下文。

在任何给定时刻,你在 Agent 中对 LLM 的输入是"这是到目前为止发生的事情,下一步是什么"

一切都是上下文工程。 LLM 是将输入转换为输出的无状态函数。要获得最好的输出,你需要给它们最好的输入。

创建出色的上下文意味着:

  • 你给模型的提示和指令
  • 你检索的任何文档或外部数据(例如 RAG)
  • 任何过去的状态、工具调用、结果或其他历史记录
  • 来自相关但独立的历史/对话的任何过去消息或事件(记忆)
  • 关于输出什么类型结构化数据的指令

标准 vs 自定义上下文格式

大多数 LLM 客户端使用这样的标准基于消息的格式:

yaml 复制代码
[
  { 'role': 'system', 'content': '你是一个有帮助的助手...' },
  { 'role': 'user', 'content': '你能部署后端吗?' },
  { 'role': 'assistant', 'content': null, 'tool_calls': [{ 'id': '1', 'name': 'list_git_tags', 'arguments': '{}' }] },
  { 'role': 'tool', 'name': 'list_git_tags', 'content': '{"tags": [{"name": "v1.2.3", "commit": "abc123"}]}', 'tool_call_id': '1' }
]

虽然这对大多数用例很有效,但如果你想真正从今天的 LLM 中获得最大收益,你需要以最 token 高效和注意力高效的方式将上下文放入 LLM。

作为标准基于消息格式的替代方案,你可以构建自己针对用例优化的上下文格式。例如,你可以使用自定义对象并根据需要将它们打包/展开到一个或多个用户、系统、助手或工具消息中。

这是将整个上下文窗口放入单个用户消息的示例:

xml 复制代码
<slack_message>
    From: @alex
    Channel: #deployments
    Text: 你能部署后端吗?
</slack_message>

<list_git_tags>
    intent: "list_git_tags"
</list_git_tags>

<list_git_tags_result>
    tags:
      - name: "v1.2.3"
        commit: "abc123"
        date: "2024-03-15T10:00:00Z"
      - name: "v1.2.2"
        commit: "def456"
        date: "2024-03-14T15:30:00Z"
</list_git_tags_result>

我们可以用这样的代码构建:

python 复制代码
class Thread:
    events: List[Event]

class Event:
    type: Literal["list_git_tags", "deploy_backend", "deploy_frontend",
                  "request_more_information", "done_for_now", "error", ...]
    data: Any

def event_to_prompt(event: Event) -> str:
    data = event.data if isinstance(event.data, str) \
           else stringify_to_yaml(event.data)
    return f"<{event.type}>\n{data}\n</{event.type}>"

def thread_to_prompt(thread: Thread) -> str:
    return '\n\n'.join(event_to_prompt(event) for event in thread.events)

掌控上下文窗口的关键好处

  1. 信息密度:以最大化 LLM 理解的方式组织信息
  2. 错误处理:以帮助 LLM 恢复的格式包含错误信息。考虑在错误解决后从上下文窗口隐藏错误和失败的调用
  3. 安全性:控制什么信息传递给 LLM,过滤掉敏感数据
  4. 灵活性:随着你了解什么对用例最有效而调整格式
  5. Token 效率:为 token 效率和 LLM 理解优化上下文格式

反复出现的主题:我不知道什么是最好的方法,但我知道你希望有灵活性来尝试一切。

关于上下文工程

在 12 因素 Agent 发布大约 2 个月后,"上下文工程"开始成为一个相当流行的术语。Andrej Karpathy 和 Shopify CEO Tobi Lütke 都在推广这个概念:

"我可能会将(提示工程)称为上下文工程的一部分。提示工程当然只是提示,但上下文工程是更广泛的过程。"


要素 4:工具只是结构化输出

工具不需要复杂。在其核心,它们只是来自 LLM 的结构化输出,触发确定性代码。

例如,假设你有两个工具 CreateIssueSearchIssues。要求 LLM "使用几个工具之一"只是要求它输出我们可以解析为表示这些工具的对象的 JSON。

python 复制代码
class Issue:
    title: str
    description: str
    team_id: str
    assignee_id: str

class CreateIssue:
    intent: Literal["create_issue"]
    issue: Issue

class SearchIssues:
    intent: Literal["search_issues"]
    query: str
    what_youre_looking_for: str

模式很简单

  1. LLM 输出结构化 JSON
  2. 确定性代码执行适当的操作(如调用外部 API)
  3. 结果被捕获并反馈到上下文中

这在 LLM 的决策和应用操作之间创建了清晰的分离。LLM 决定做什么,但代码控制如何完成。仅仅因为 LLM "调用了工具"并不意味着你每次都必须以相同的方式执行特定的相应函数。

python 复制代码
if next_step.intent == 'create_payment_link':
    stripe.paymentlinks.create(next_step.parameters)
    return  # 或者你想要的任何东西
elif next_step.intent == 'wait_for_a_while':
    # 做一些单子操作
    pass
else:  # 模型没有调用我们知道的工具
    # 做其他事情
    pass

"下一步"可能不像"运行纯函数并返回结果"那样原子化。当你把"工具调用"想象成模型输出描述确定性代码应该做什么的 JSON 时,你就解锁了很多灵活性。


要素 5:统一执行状态与业务状态

即使在 AI 世界之外,许多基础设施系统也试图将"执行状态"与"业务状态"分开。对于 AI 应用,这可能涉及复杂的抽象来跟踪当前步骤、下一步、等待状态、重试计数等。这种分离创建了可能值得但对用例可能过于复杂的复杂性。

更清楚地说:

  • 执行状态:当前步骤、下一步、等待状态、重试计数等
  • 业务状态:Agent 工作流中到目前为止发生的事情(例如 OpenAI 消息列表、工具调用和结果列表等)

如果可能,简化------尽可能统一这些。

实际上,你可以设计应用,使你可以从上下文窗口推断所有执行状态。在许多情况下,执行状态(当前步骤、等待状态等)只是关于到目前为止发生了什么的元数据。

你可能有一些不能放入上下文窗口的东西,如会话 ID、密码上下文等,但目标应该是最小化这些东西。

这种方法有几个好处

  1. 简单性:所有状态的单一事实来源
  2. 序列化:线程是简单可序列化/反序列化的
  3. 调试:整个历史记录在一个地方可见
  4. 灵活性:通过添加新事件类型轻松添加新状态
  5. 恢复:可以通过加载线程从任何点恢复
  6. 分叉:可以通过将线程的某个子集复制到新上下文/状态 ID 来在任何点分叉线程
  7. 人类界面和可观测性:将线程转换为人类可读的 markdown 或丰富的 Web 应用 UI 是简单的

要素 6:启动/暂停/恢复的简单 API

Agent 只是程序,我们对如何启动、查询、恢复和停止它们有一定的期望。

  • 用户、应用、管道和其他 Agent 应该能够通过简单的 API 启动 Agent
  • Agent 及其编排的确定性代码应该能够在需要长时间运行操作时暂停 Agent
  • 像 webhook 这样的外部触发器应该使 Agent 能够从它们停止的地方恢复,而不需要与 Agent 编排器深度集成

与要素 5(统一执行状态和业务状态)和要素 8(掌控控制流)密切相关,但可以独立实现。

注意:AI 编排器通常允许暂停和恢复,但不是在工具选择和工具执行之间的时刻。


要素 7:用工具调用联系人类

默认情况下,LLM API 依赖于一个基本的高风险 token 选择:我们是返回纯文本内容,还是返回结构化数据?

你在第一个 token 的选择上放了很多权重,在 the weather in tokyo 的情况下,它是:

"the"

但在 fetch_weather 的情况下,它是某个表示 JSON 对象开始的特殊 token:

|JSON>

你可能会通过让 LLM 始终 输出 JSON 来获得更好的结果,然后用一些自然语言 token 如 request_human_inputdone_for_now 声明其意图(而不是像 check_weather_in_city 这样的"正式"工具)。

python 复制代码
class Options:
    urgency: Literal["low", "medium", "high"]
    format: Literal["free_text", "yes_no", "multiple_choice"]
    choices: List[str]

# 人类交互的工具定义
class RequestHumanInput:
    intent: Literal["request_human_input"]
    question: str
    context: str
    options: Options

# Agent 循环中的示例用法
if next_step.intent == 'request_human_input':
    thread.events.append({
        'type': 'human_input_requested',
        'data': next_step
    })
    thread_id = await save_state(thread)
    await notify_human(next_step, thread_id)
    return  # 中断循环并等待响应带着线程 ID 返回
else:
    # ... 其他情况

稍后,你可能会从处理 Slack、电子邮件、短信或其他事件的系统收到 webhook:

python 复制代码
@app.post('/webhook')
async def webhook(req: Request):
    thread_id = req.body.thread_id
    thread = await load_state(thread_id)
    thread.events.append({
        'type': 'response_from_human',
        'data': req.body
    })
    # ... 简化为简洁起见,你可能不想在这里阻塞 web worker
    next_step = await determine_next_step(thread_to_prompt(thread))
    thread.events.append(next_step)
    result = await handle_next_step(thread, next_step)
    # 循环或中断或你想要的任何东西

    return {"status": "ok"}

好处

  1. 清晰的指令:用于不同类型人类联系的工具允许 LLM 更具体
  2. 内循环 vs 外循环 :启用 传统 chatGPT 风格界面之外 的 Agent 工作流,其中控制流和上下文初始化可能是 Agent->Human 而不是 Human->Agent(想想由 cron 或事件启动的 Agent)
  3. 多人访问:可以通过结构化事件轻松跟踪和协调来自不同人类的输入
  4. 多 Agent :简单的抽象可以轻松扩展以支持 Agent->Agent 请求和响应
  5. 持久性:与要素 6(启动/暂停/恢复)结合,这使得持久、可靠和可内省的多人工作流成为可能

要素 8:掌控控制流

如果你拥有控制流,你可以做很多有趣的事情。

为特定用例构建有意义的控制结构。具体来说,某些类型的工具调用可能是中断循环并等待人类或另一个长时间运行任务(如训练管道)响应的理由。你可能还想整合以下自定义实现:

  • 工具调用结果的摘要或缓存
  • 对结构化输出的 LLM 作为评判者
  • 上下文窗口压缩或其他记忆管理
  • 日志记录、跟踪和指标
  • 客户端速率限制
  • 持久睡眠/暂停/"等待事件"

以下示例显示了三种可能的控制流模式:

python 复制代码
async def handle_next_step(thread: Thread):
    while True:
        next_step = await determine_next_step(thread_to_prompt(thread))

        if next_step.intent == 'request_clarification':
            # 模型请求更多信息,中断循环等待人类响应
            thread.events.append({
                'type': 'request_clarification',
                'data': next_step,
            })
            await send_message_to_human(next_step)
            await db.save_thread(thread)
            break  # 异步步骤 - 中断循环,稍后我们会收到 webhook

        elif next_step.intent == 'fetch_open_issues':
            # 同步步骤 - 获取数据并传回 LLM
            thread.events.append({
                'type': 'fetch_open_issues',
                'data': next_step,
            })
            issues = await linear_client.issues()
            thread.events.append({
                'type': 'fetch_open_issues_result',
                'data': issues,
            })
            continue  # 将新上下文传递给 LLM 以确定下下一步

        elif next_step.intent == 'create_issue':
            # 高风险操作 - 中断循环等待人类批准
            thread.events.append({
                'type': 'create_issue',
                'data': next_step,
            })
            await request_human_approval(next_step)
            await db.save_thread(thread)
            break  # 异步步骤 - 中断循环,稍后我们会收到 webhook

这种模式允许你根据需要中断和恢复 Agent 的流程,创建更自然的对话和工作流。

示例 :我对每个 AI 框架的第一个功能请求是我们需要能够中断正在工作的 Agent 并稍后恢复,特别是 在工具选择 和工具调用之间的时刻。

没有这种可恢复性/粒度级别,就没有办法在工具调用运行之前审查/批准它,这意味着你被迫要么:

  1. 在等待长时间运行的事情完成时将任务保持在内存中(想想 while...sleep),如果进程被中断就从头开始重新启动
  2. 将 Agent 限制为只能进行低风险、低风险的调用,如研究和摘要
  3. 给 Agent 访问做更大、更有用事情的权限,然后就 yolo 希望它不会搞砸

要素 9:将错误压缩进上下文窗口

这一点比较简短但值得一提。Agent 的好处之一是"自我修复"------对于短任务,LLM 可能会调用一个失败的工具。好的 LLM 有相当好的机会阅读错误消息或堆栈跟踪并找出在后续工具调用中要更改的内容。

大多数框架实现了这一点,但你可以只做这一点而不做其他 11 个因素。这是一个例子:

python 复制代码
thread = {"events": [initial_message]}

while True:
    next_step = await determine_next_step(thread_to_prompt(thread))
    thread["events"].append({
        "type": next_step.intent,
        "data": next_step,
    })
    try:
        result = await handle_next_step(thread, next_step)  # 我们的 switch 语句
    except Exception as e:
        # 如果我们得到一个错误,我们可以将它添加到上下文窗口并重试
        thread["events"].append({
            "type": 'error',
            "data": format_error(e),
        })
        # 循环,或在这里做任何其他事情来尝试恢复

你可能想为特定工具调用实现错误计数器,限制单个工具的约 3 次尝试,或任何其他对用例有意义的逻辑:

python 复制代码
consecutive_errors = 0

while True:
    # ... 现有代码 ...

    try:
        result = await handle_next_step(thread, next_step)
        thread["events"].append({
            "type": next_step.intent + '_result',
            "data": result,
        })
        # 成功!重置错误计数器
        consecutive_errors = 0
    except Exception as e:
        consecutive_errors += 1
        if consecutive_errors < 3:
            # 执行循环并重试
            thread["events"].append({
                "type": 'error',
                "data": format_error(e),
            })
        else:
            # 中断循环,重置上下文窗口的部分,升级到人类,或你想做的任何其他事情
            break

达到某个连续错误阈值可能是升级到人类的好地方,无论是通过模型决策还是通过控制流的确定性接管。

好处

  1. 自我修复:LLM 可以阅读错误消息并找出在后续工具调用中要更改的内容
  2. 持久性:即使一个工具调用失败,Agent 也可以继续运行

我确信你会发现如果你这样做太多, Agent 会开始失控,可能会一遍又一遍地重复相同的错误。这就是要素 8(掌控控制流)和要素 3(掌控上下文构建)发挥作用的地方------你不需要只是将原始错误放回去,你可以完全重构它的表示方式,从上下文窗口中删除之前的事件,或任何你发现有效的确定性事情来让 Agent 回到正轨。

但防止错误失控的第一方法是拥抱要素 10------小而专注的 Agent


要素 10:小而专注的 Agent

与其构建试图做所有事情的单体 Agent,不如构建做好一件事的小而专注的 Agent。Agent 只是更大、主要是确定性系统中的一个构建块。

这里的关键洞察是关于 LLM 的限制:任务越大越复杂,需要的步骤就越多,这意味着更长的上下文窗口。随着上下文的增长,LLM 更可能迷失或失去焦点。通过让 Agent 专注于 3-10 步,最多 20 步的特定领域,我们保持上下文窗口可管理,LLM 性能高。

随着上下文增长,LLM 更可能迷失或失去焦点

小而专注的 Agent 的好处

  1. 可管理的上下文:更小的上下文窗口意味着更好的 LLM 性能
  2. 清晰的职责:每个 Agent 都有明确的范围和目的
  3. 更好的可靠性:在复杂工作流中迷失的机会更少
  4. 更容易测试:更容易测试和验证特定功能
  5. 改进的调试:当问题发生时更容易识别和修复

如果 LLM 变得更聪明怎么办?

如果 LLM 变得足够聪明可以处理 100 步以上的工作流,我们还需要这个吗?

简短的回答是是的 。随着 Agent 和 LLM 的改进,它们可能 自然扩展到能够处理更长的上下文窗口。这意味着处理更大 DAG 的更多部分。这种小而专注的方法确保你今天就能获得结果,同时让你准备好随着 LLM 上下文窗口变得更可靠而慢慢扩展 Agent 范围。(如果你以前重构过大型确定性代码库,你现在可能正在点头。)

有意识地控制 Agent 的大小/范围,并且只以允许你保持质量的方式增长,是这里的关键。正如构建 NotebookLM 的团队所说:

"我觉得一致地,AI 构建中最神奇的时刻对我来说是当我真的、真的、真的接近模型能力边缘的时候"

无论那个边界在哪里,如果你能找到那个边界并始终做对,你就会构建神奇的体验。这里有很多护城河可以建立,但像往常一样,它们需要一些工程严谨性。


要素 11:从任何地方触发,在用户所在的地方服务

如果你正在做要素 6(启动/暂停/恢复的简单 API)和要素 7(用工具调用联系人类),你就准备好整合这个因素了。

使用户能够从 Slack、电子邮件、短信或他们想要的任何其他渠道触发 Agent。使 Agent 能够通过相同的渠道响应。

好处

  1. 在用户所在的地方服务:这帮助你构建感觉像真人或至少像数字同事的 AI 应用
  2. 外循环 Agent:使 Agent 能够被非人类触发,例如事件、cron、故障等。它们可能工作 5、20、90 分钟,但当它们到达关键点时,可以联系人类寻求帮助、反馈或批准
  3. 高风险工具:如果你能快速循环各种人类,你可以给 Agent 访问更高风险的操作,如发送外部电子邮件、更新生产数据等。保持清晰的标准让你获得可审计性和对执行更大更好事情的 Agent 的信心

要素 12:让 Agent 成为无状态 Reducer

好吧,我们已经超过 1000 行 markdown 了。这个主要只是为了好玩。

从函数式编程的角度来看,Agent 本质上是一个 reducer------一个接收当前状态和事件,返回新状态的纯函数:

ini 复制代码
newState = reducer(currentState, event)

或者用 Haskell 风格表示:

haskell 复制代码
foldl agentStep initialState events

这种无状态 reducer 的思维方式有助于我们:

  • 更清晰地思考 Agent 的行为
  • 更容易测试和调试
  • 更好地实现持久化和恢复

附录:预取上下文

如果模型很可能会调用工具 X,不要浪费 token 往返告诉模型去获取它。

不要这样

python 复制代码
# 提示中包含:
# "当查看部署时,你可能想获取已发布的 git 标签列表..."
# 工具选项包括:list_git_tags, deploy_backend_to_prod, done_for_now

thread = {"events": [initial_message]}
next_step = await determine_next_step(thread)

while True:
    if next_step.intent == 'list_git_tags':
        tags = await fetch_git_tags()
        thread["events"].append({
            "type": 'list_git_tags',
            "data": tags,
        })
    elif next_step.intent == 'deploy_backend_to_prod':
        # ...

而是这样

python 复制代码
# 提示中直接包含:
# "当前的 git 标签是:{{ git_tags }}"
# 工具选项只有:deploy_backend_to_prod, done_for_now

thread = {"events": [initial_message]}
git_tags = await fetch_git_tags()  # 预取!

next_step = await determine_next_step(thread, git_tags)

while True:
    if next_step.intent == 'deploy_backend_to_prod':
        # ...

或者更好的是,将标签包含在线程中,从提示模板中删除特定参数:

python 复制代码
thread = {"events": [initial_message]}

# 添加请求
thread["events"].append({
    "type": 'list_git_tags',
})

git_tags = await fetch_git_tags()

# 添加结果
thread["events"].append({
    "type": 'list_git_tags_result',
    "data": git_tags,
})

next_step = await determine_next_step(thread)

如果你已经知道你想让模型调用什么工具,就确定性地调用它们,让模型做弄清楚如何使用它们输出的困难部分

再次强调,AI 工程全都是关于上下文工程


总结

12 因素 Agent 方法论为我们提供了一套系统化的方法来构建可靠、可维护的 AI Agent 系统。让我们回顾核心要点:

核心理念

  1. Agent 只是软件------不要被"AI Agent"的概念神秘化,它们本质上是有输入、有输出、有状态的程序
  2. 简单优于复杂------从小而专注的 Agent 开始,只在证明可靠后才扩展范围
  3. 掌控一切------掌控提示、上下文、控制流,不要把这些关键能力外包给框架

十二要素速查

要素 核心思想 一句话总结
1 自然语言到工具调用 LLM 负责意图理解,代码负责执行
2 掌控提示词 提示是一等代码,不是黑盒配置
3 掌控上下文窗口 一切都是上下文工程
4 工具只是结构化输出 工具调用只是 JSON,执行由你决定
5 统一执行状态与业务状态 简化状态管理,单一事实来源
6 启动/暂停/恢复 像普通程序一样管理 Agent 生命周期
7 用工具调用联系人类 人机交互也是一种工具调用
8 掌控控制流 自定义循环逻辑,按需中断和恢复
9 错误压缩进上下文 让 LLM 自我修复,但设置限制
10 小而专注的 Agent 限制范围,保持上下文可管理
11 从任何地方触发 在用户所在的地方服务
12 无状态 Reducer 函数式思维让 Agent 更可预测

最后的思考

构建生产级 AI Agent 不是要追求最复杂的架构或最先进的框架。相反,它是关于:

  • 理解 LLM 的限制------上下文窗口越长,性能越差
  • 保持简单------从小处着手,证明有效后再扩展
  • 拥抱确定性------让 LLM 做它擅长的(理解和推理),让代码做代码擅长的(可靠执行)
  • 为人类设计------Agent 应该增强人类能力,而不是取代人类判断

"我觉得一致地,AI 构建中最神奇的时刻是当我真的接近模型能力边缘的时候"

找到那个边界,并始终在那里交付,这就是构建神奇 AI 体验的秘诀。

相关推荐
别动哪条鱼12 小时前
FFmpeg模块化架构
架构·ffmpeg
milanyangbo12 小时前
像Git一样管理数据:深入解析数据库并发控制MVCC的实现
服务器·数据库·git·后端·mysql·架构·系统架构
张人大 Renda Zhang12 小时前
Spring Cloud / Dubbo 是 2 楼,Kubernetes 是 1 楼,Service Mesh 是地下室:Java 微服务的“三层楼模型”
spring boot·spring cloud·云原生·架构·kubernetes·dubbo·service_mesh
weixin_3077791312 小时前
Jenkins Metrics 插件全解析:从数据采集到智能监控的实践指南
运维·开发语言·架构·jenkins
特拉熊12 小时前
23种设计模式之桥接模式
java·架构
小白|12 小时前
【OpenHarmony × Flutter】混合开发高阶实战:如何统一管理跨平台状态?Riverpod + ArkTS 共享数据流架构详解
flutter·架构
虚伪的空想家12 小时前
arm架构TDengine时序数据库及应用使用K8S部署
服务器·arm开发·架构·kubernetes·arm·时序数据库·tdengine
拾忆,想起12 小时前
Dubbo服务降级全攻略:构建韧性微服务系统的守护盾
java·前端·网络·微服务·架构·dubbo
闲人编程12 小时前
FastAPI框架架构与设计哲学
python·架构·api·fastapi·异步·codecapsule