Agent设计12要素:构建可靠的AI Agent

大模型向量数据库数据中台

本文仅做记录
译自:raw repo:https://github.com/humanlayer/12-factor-agents

因素 1. 自然语言到工具调用

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

picture.image

当原子化地应用此模式时,它可以将类似以下的短语

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

转换为描述Stripe API调用的结构化对象,例如

  
{  
  "function": {  
    "name": "create\_payment\_link",  
    "parameters": {  
      "amount": 750,  
      "customer": "cust\_128934ddasf9",  
      "product": "prod\_8675309",  
      "price": "prc\_09874329fds",  
      "quantity": 1,  
      "memo": "嘿Jeff - 请参见下方用于二月AI Tinkerers聚会的支付链接"  
    }  
  }  
}  

注意 :实际上,Stripe API稍微复杂一些,一个真正执行此操作的智能体(视频)会列出客户、产品、价格等,以使用正确的ID构建此有效载荷,或在提示/上下文窗口中包含这些ID(我们将在下面看到,它们其实是一回事!)

从那里,确定性代码可以接收有效载荷并对其进行处理。(更多内容请参见因素3)

  
# LLM接收自然语言并返回一个结构化对象  
nextStep = await llm.determineNextStep(  
"""  
  为Jeff创建一个750美元的支付链接  
  用于赞助二月的AI Tinkerers聚会  
  """  
  )  
  
# 根据其功能处理结构化输出  
if nextStep.function == 'create\_payment\_link':  
    stripe.paymentlinks.create(nextStep.parameters)  
    return# 或者你想要的任何内容,请参见下文  
elif nextStep.function == 'something\_else':  
    # ... 更多情况  
    pass  
else:  # 模型未调用我们知道的工具  
    # 做其他事情  
    pass  

注意 :虽然一个完整的智能体会接收API调用结果并与其循环,最终返回类似

我已成功为Terri创建了一个750美元的支付链接,用于赞助二月的AI Tinkerers聚会。链接如下:https://buy.stripe.com/test\_1234567890

相反 ,我们在这里实际上会跳过这一步,并将其留给另一个因素,你可以选择是否也采用(由你决定!)

因素 2. 拥有你的提示

不要将你的提示工程外包给一个框架。

picture.image

顺便说一句,这远非新颖的建议:

picture.image

一些框架提供了类似这样的“黑盒”方法:

  
agent = Agent(  
  role="...",  
  goal="...",  
  personality="...",  
  tools=[tool1, tool2, tool3]  
)  
  
task = Task(  
  instructions="...",  
  expected\_output=OutputModel  
)  
  
result = agent.run(task)  

这对于引入一些顶级的提示工程来帮助你入门是很好的,但通常很难进行微调和/或逆向工程,以获得进入模型的精确标记。

相反,拥有你的提示,并将其视为一等公民的代码:

  
function DetermineNextStep(thread: string) -> DoneForNow | ListGitTags | DeployBackend | DeployFrontend | RequestMoreInformation {  
  prompt #"  
    {{ \_.role("system") }}  
      
    你是一个管理前端和后端系统部署的有用助手。  
    你勤奋地确保通过遵循最佳实践和正确的部署程序来实现安全和成功的部署。  
      
    在部署任何系统之前,你应该检查:  
    - 部署环境(暂存 vs 生产)  
    - 要部署的正确标签/版本  
    - 当前系统状态  
      
    你可以使用诸如 deploy\_backend、deploy\_frontend 和 check\_deployment\_status  
    等工具来管理部署。对于敏感部署,使用 request\_approval 来获得  
    人工验证。  
      
    始终思考首先该做什么,比如:  
    - 检查当前部署状态  
    - 验证部署标签是否存在  
    - 如有需要,请求批准  
    - 在生产之前先部署到暂存环境  
    - 监控部署进度  
      
    {{ \_.role("user") }}  
  
    {{ thread }}  
      
    下一步应该是什么?  
  "#  
}  

(上面的例子使用了 BAML 来生成提示,但你可以使用任何你想要的提示工程工具,甚至手动进行模板化)

如果签名看起来有点奇怪,我们将在因素4 - 工具只是结构化输出中讨论这一点

  
function DetermineNextStep(thread: string) -> DoneForNow | ListGitTags | DeployBackend | DeployFrontend | RequestMoreInformation {  

拥有你的提示的关键好处:

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

记住:你的提示是你应用程序逻辑和LLM之间的主要接口。

完全控制你的提示能为你提供生产级智能体所需的灵活性和提示控制。

我不知道什么是最好的提示,但我知道你想要尝试一切的灵活性。

因素 3. 拥有你的上下文窗口

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

在任何给定的时刻,你向智能体中的LLM输入的内容是:“到目前为止发生了什么,下一步是什么”

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

创建优秀的上下文意味着:

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

picture.image

关于上下文工程

本指南的全部内容都是关于如何从今天的模型中获取尽可能多的价值。值得注意的是,以下内容并未提及:

  • 模型参数的更改,如 temperature、top_p、frequency_penalty、presence_penalty 等。
  • 训练你自己的补全或嵌入模型
  • 微调现有模型

再次强调,我不知道将上下文交给LLM的最佳方式是什么,但我知道你需要有灵活性来尝试一切。

标准与自定义上下文格式

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

  
[  
  {  
    "role":"system",  
    "content":"You are a helpful assistant..."  
},  
{  
    "role":"user",  
    "content":"Can you deploy the backend?"  
},  
{  
    "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\", \"date\": \"2024-03-15T10:00:00Z\"}, {\"name\": \"v1.2.2\", \"commit\": \"def456\", \"date\": \"2024-03-14T15:30:00Z\"}, {\"name\": \"v1.2.1\", \"commit\": \"abe033d\", \"date\": \"2024-03-13T09:15:00Z\"}]}",  
    "tool\_call\_id":"1"  
}  
]  

虽然这在大多数用例中效果很好,但如果你想真正从今天的LLM中榨取最大价值,你需要以最节省token和注意力的方式将你的上下文输入到LLM中。

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

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

  
  
[  
{  
    "role":"system",  
    "content":"You are a helpful assistant..."  
},  
{  
    "role":"user",  
    "content":|  
            Here's everything that happened so far:  
          
        <slack\_message>  
            From:@alex  
            Channel:#deployments  
            Text:Canyoudeploythebackend?  
        </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"  
              -name:"v1.2.1"  
                commit:"ghi789"  
                date:"2024-03-13T09:15:00Z"  
        </list\_git\_tags\_result>  
          
        what'sthenextstep?  
    }  
]  

模型可能会通过你提供的工具模式推断出你在询问它下一步是什么,但将其融入你的提示模板也无妨。

代码示例

我们可以用类似这样的方式来构建:

  
  
class Thread:  
  events: List[Event]  
  
class Event:  
# 可以只用字符串,也可以显式定义 - 由你决定  
  type: Literal["list\_git\_tags", "deploy\_backend", "deploy\_frontend", "request\_more\_information", "done\_for\_now", "list\_git\_tags\_result", "deploy\_backend\_result", "deploy\_frontend\_result", "request\_more\_information\_result", "done\_for\_now\_result", "error"]  
  data: ListGitTags | DeployBackend | DeployFrontend | RequestMoreInformation |    
        ListGitTagsResult | DeployBackendResult | DeployFrontendResult | RequestMoreInformationResult | string  
  
def event\_to\_prompt(event: Event) -> str:  
    data = event.data if isinstance(event.data, str) \  
           else stringifyToYaml(event.data)  
  
    returnf"<{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)  

示例上下文窗口

以下是使用这种方法的上下文窗口可能的样子:

初始Slack请求:

  
<slack\_message>  
    From: @alex  
    Channel: #deployments  
    Text: Can you deploy the latest backend to production?  
</slack\_message>  

列出Git标签后:

  
<slack\_message>  
    From: @alex  
    Channel: #deployments  
    Text: Can you deploy the latest backend to production?  
    Thread: []  
</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"  
      - name: "v1.2.1"  
        commit: "ghi789"  
        date: "2024-03-13T09:15:00Z"  
</list\_git\_tags\_result>  

出错并恢复后:

  
<slack\_message>  
    From: @alex  
    Channel: #deployments  
    Text: Can you deploy the latest backend to production?  
    Thread: []  
</slack\_message>  
  
<deploy\_backend>  
    intent: "deploy\_backend"  
    tag: "v1.2.3"  
    environment: "production"  
</deploy\_backend>  
  
<error>  
    error running deploy\_backend: Failed to connect to deployment service  
</error>  
  
<request\_more\_information>  
    intent: "request\_more\_information\_from\_human"  
    question: "I had trouble connecting to the deployment service, can you provide more details and/or check on the status of the service?"  
</request\_more\_information>  
  
<human\_response>  
    data:  
      response: "I'm not sure what's going on, can you check on the status of the latest workflow?"  
</human\_response>  

从这里开始,你的下一步可能是:

  
nextStep = await determine\_next\_step(thread\_to\_prompt(thread))  

  
{  
  "intent": "get\_workflow\_status",  
  "workflow\_name": "tag\_push\_prod.yaml",  
}  

XML风格的格式只是一个例子 - 关键是你可以构建自己的格式,使其符合你的应用程序需求。如果你有灵活性去尝试不同的上下文结构以及存储和传递给LLM的内容,你将获得更好的质量。

拥有你的上下文窗口的关键好处:

  1. 信息密度 :以最大化LLM理解的方式组织信息
  2. 错误处理 :以帮助LLM恢复的格式包含错误信息。考虑在错误和失败的调用解决后将其从上下文窗口中隐藏。
  3. 安全性 :控制传递给LLM的信息,过滤掉敏感数据
  4. 灵活性 :根据你学到的最佳实践调整格式
  5. Token效率 :优化上下文格式以提高token效率和LLM理解

上下文包括:提示、指令、RAG文档、历史、工具调用、记忆

记住:上下文窗口是你与LLM的主要接口。掌控如何结构化和呈现信息可以显著提升你的智能体性能。

示例 - 信息密度 - 相同消息,更少的tokens:

picture.image

Loom Screenshot 2025-04-22 at 09 00 56

不要听我说的

在12因子智能体发布约2个月后,上下文工程开始成为一个相当流行的术语。

picture.image

picture.image

还有来自@lenadroid的相当不错的上下文工程备忘单,发布于2025年7月。

picture.image

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

因素 4. 工具只是结构化输出

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

picture.image

例如,假设你有两个工具 CreateIssueSearchIssues。要求一个LLM“使用多个工具中的一个”只是要求它输出我们可以解析成代表这些工具的对象的JSON。

  
  
class Issue:  
  title: str  
  description: str  
  team\_id: str  
  assignee\_id: str  
  
class CreateIssue:  
  intent: "create\_issue"  
  issue: Issue  
  
class SearchIssues:  
  intent: "search\_issues"  
  query: str  
  what\_youre\_looking\_for: str  

这个模式很简单:

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

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

如果你还记得我们上面的switch语句

  
if nextStep.intent == 'create\_payment\_link':  
    stripe.paymentlinks.create(nextStep.parameters)  
    return # 或者你想要的任何内容,请参见下文  
elif nextStep.intent == 'wait\_for\_a\_while':   
    # 做一些单子操作,我不知道  
else: #... 模型没有调用我们知道的工具  
    # 做其他事情  

注意 :关于“纯提示”与“工具调用”与“JSON模式”的好处以及每种方法的性能权衡,已经有很多讨论。我们很快会链接一些关于这些内容的资源,但在这里不会深入探讨。参见提示 vs JSON模式 vs 函数调用 vs 约束生成 vs SAP,我什么时候应该使用函数调用、结构化输出或JSON模式? 和 OpenAI JSON vs 函数调用。

“下一步”可能不像“运行一个纯函数并返回结果”那样原子化。当你将“工具调用”视为模型输出描述确定性代码应该做什么的JSON时,你会解锁很多灵活性。将这一点与因素8 拥有你的控制流结合起来。

因素 5. 统一执行状态和业务状态

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

一如既往,决定什么适合你的应用由你决定。但不要认为你必须将它们分开管理。

更明确地说:

  • 执行状态 :当前步骤、下一步、等待状态、重试次数等。
  • 业务状态 :代理工作流到目前为止发生了什么(例如,OpenAI消息列表、工具调用和结果列表等)。

如果可能,尽量简化——尽可能统一这些状态。

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

你可能有一些不能放入上下文窗口的东西,比如会话ID、密码上下文等,但你的目标应该是尽量减少这些东西。通过拥抱因素3,你可以控制实际进入LLM的内容。

这种方法有几个好处:

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

因素 6. 使用简单API启动/暂停/恢复

智能体只是程序,我们对其启动、查询、恢复和停止的方式有相应的期望。

用户、应用程序、管道和其他智能体应该能够通过简单的API轻松启动智能体。

智能体及其协调的确定性代码应该能够在需要长时间运行操作时暂停智能体。

外部触发器(如webhooks)应允许智能体从中断处恢复,而无需与智能体协调器进行深度集成。

这与因素5 - 统一执行状态和业务状态和因素8 - 拥有你的控制流密切相关,但可以独立实现。

注意 - 通常AI协调器允许暂停和恢复,但不能在工具选择和工具执行的瞬间之间进行。另见因素7 - 使用工具调用联系人类和因素11 - 从任何地方触发,与用户会面。

因素 7. 使用工具调用联系人类

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

picture.image

你把这个选择的权重放在了第一个 token 上,对于 the weather in tokyo 这种情况,它是

"the"

但在 fetch\_weather 的情况下,它是一些特殊 token 来表示 JSON 对象的开始。

|JSON>

你可能会通过让 LLM 始终 输出 json 获得更好的结果,然后用一些自然语言 token 声明其意图,比如 request\_human\_inputdone\_for\_now(而不是像 check\_weather\_in\_city 这样的“正式”工具)。

再次强调,你可能不会从这获得任何性能提升,但你应该进行实验,并确保你有自由尝试奇怪的东西来获得最佳结果。

  
  
class Options:  
  urgency: Literal["low", "medium", "high"]  
  format: Literal["free\_text", "yes\_no", "multiple\_choice"]  
  choices: List[str]  
  
# 用于人机交互的工具定义  
class RequestHumanInput:  
  intent: "request\_human\_input"  
  question: str  
  context: str  
  options: Options  
  
# 在代理循环中的示例用法  
if nextStep.intent == 'request\_human\_input':  
  thread.events.append({  
    type: 'human\_input\_requested',  
    data: nextStep  
  })  
  thread\_id = await save\_state(thread)  
await notify\_human(nextStep, thread\_id)  
return# 中断循环并等待带有线程ID的响应  
else:  
# ... 其他情况  

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

  
  
@app.post('/webhook')  
def webhook(req: Request):  
  thread\_id = req.body.threadId  
  thread = await load\_state(thread\_id)  
  thread.events.push({  
    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)  
# todo - 循环或中断或任何你想要的  
  
return {"status": "ok"}  

上述内容包含了因素 5 - 统一执行状态和业务状态、因素 8 - 拥有你的控制流、因素 3 - 拥有你的上下文窗口和因素 4 - 工具只是结构化输出以及其他几个因素的模式。

如果我们使用因素 3 - 拥有你的上下文窗口中的 XML 风格格式,经过几次交互后,我们的上下文窗口可能看起来像这样:

  
  
(为简洁起见省略)  
  
<slack\_message>  
    From: @alex  
    Channel: #deployments  
    Text: Can you deploy backend v1.2.3 to production?  
    Thread: []  
</slack\_message>  
  
<request\_human\_input>  
    intent: "request\_human\_input"  
    question: "Would you like to proceed with deploying v1.2.3 to production?"  
    context: "This is a production deployment that will affect live users."  
    options: {  
        urgency: "high"  
        format: "yes\_no"  
    }  
</request\_human\_input>  
  
<human\_response>  
    response: "yes please proceed"  
    approved: true  
    timestamp: "2024-03-15T10:30:00Z"  
    user: "alex@company.com"  
</human\_response>  
  
<deploy\_backend>  
    intent: "deploy\_backend"  
    tag: "v1.2.3"  
    environment: "production"  
</deploy\_backend>  
  
<deploy\_backend\_result>  
    status: "success"  
    message: "Deployment v1.2.3 to production completed successfully."  
    timestamp: "2024-03-15T10:30:00Z"  
</deploy\_backend\_result>  

好处:

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

更多关于外循环代理的内容在这里

picture.image

与因素 11 - 从任何地方触发,与用户会面配合使用效果很好

← 启动/暂停/恢复 | 拥有你的控制流 →

因素 8. 拥有你的控制流

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

picture.image

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

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

下面的示例展示了三种可能的控制流模式:

  • request_clarification:模型请求更多信息,跳出循环并等待来自人类的响应
  • fetch_git_tags:模型请求获取git标签列表,获取标签,附加到上下文窗口,并直接传回给模型
  • deploy_backend:模型请求部署后端,这是一件高风险的事情,因此跳出循环并等待人工批准
  
def handle\_next\_step(thread: Thread):  
  
whileTrue:  
    next\_step = await determine\_next\_step(thread\_to\_prompt(thread))  
      
    # 为了清晰起见内联 - 实际上你可以将其放入一个方法中,使用异常进行控制流,或任何你想要的方式  
    if next\_step.intent == 'request\_clarification':  
      thread.events.append({  
        type: 'request\_clarification',  
          data: nextStep,  
        })  
  
      await send\_message\_to\_human(next\_step)  
      await db.save\_thread(thread)  
      # 异步步骤 - 跳出循环,我们稍后会收到一个webhook  
      break  
    elif next\_step.intent == 'fetch\_open\_issues':  
      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,  
      })  
      # 同步步骤 - 将新上下文传递给LLM以确定下一步  
      continue  
    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)  
      # 异步步骤 - 跳出循环,我们稍后会收到一个webhook  
      break  

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

示例 - 我对每个AI框架的最大功能请求是我们需要能够中断正在工作的智能体并稍后恢复,尤其是在工具选择和工具调用时刻之间

如果没有这种级别的可恢复性/粒度,就无法在工具调用运行前进行审查/批准,这意味着你只能:

  1. 在等待长时间运行的任务完成时在内存中暂停任务(想想 while...sleep ),如果过程被中断,则从头开始重新启动
  2. 将智能体限制为仅进行低风险、低风险的调用,如研究和摘要
  3. 允许智能体访问进行更大、更有用的操作,并希望它不会搞砸

你可能会注意到这与因素5 - 统一执行状态和业务状态和因素6 - 使用简单API启动/暂停/恢复密切相关,但可以独立实现。

← 返回 README

因素 9. 将错误压缩到上下文窗口

这一点虽然有点简短,但值得提及。智能体的好处之一是“自我修复”——对于短期任务,LLM可能会调用一个失败的工具。好的LLM有很大几率能够读取错误消息或堆栈跟踪,并在后续的工具调用中找出需要更改的内容。

大多数框架都实现了这一点,但你可以在不进行其他11个因素的情况下仅实现这一点。以下是一个示例:

  
thread = {"events": [initial\_message]}  
  
whileTrue:  
  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次,或根据你的用例实施其他逻辑。

  
consecutive\_errors = 0  
  
whileTrue:  
  
# ... 现有代码 ...  
  
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. 持久性 :即使一个工具调用失败,智能体也可以继续运行

我确信你会发现,如果这样做得太多,你的智能体会开始失控,并可能一遍又一遍地重复相同的错误。

这就是因素8 - 拥有你的控制流和因素3 - 拥有你的上下文构建发挥作用的地方——你不需要仅仅将原始错误放回,你可以完全重构其表示方式,从上下文窗口中移除先前的事件,或执行任何确定性操作来使智能体重回正轨。

但防止错误失控的首要方法是拥抱因素10 - 小而专注的智能体。

因素 10. 小而专注的智能体

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

picture.image

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

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

小而专注的智能体的好处:

  1. 可管理的上下文 :较小的上下文窗口意味着更好的LLM性能
  2. 明确的责任 :每个智能体都有明确定义的范围和目的
  3. 更好的可靠性 :在复杂工作流中迷失的可能性更小
  4. 更易测试 :更容易测试和验证特定功能
  5. 改进的调试 :在出现问题时更容易识别和修复

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

如果LLM足够聪明,能够处理100步以上的工作流,我们还需要这样做吗?

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

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

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

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

因素 11. 从任何地方触发,与用户会面

如果你在等待 humanlayer 的推荐,你已经找到了。如果你正在执行因素6 - 使用简单API启动/暂停/恢复和因素7 - 使用工具调用联系人类,你已经准备好融入这一因素。

picture.image

允许用户通过Slack、电子邮件、短信或他们想要的任何其他渠道触发智能体。允许智能体通过相同的渠道进行响应。

好处:

  • 与用户会面 :这有助于你构建感觉像真人或至少是数字同事的AI应用
  • 外循环智能体 :允许智能体由非人类触发,例如事件、cron、故障或其他任何情况。它们可能工作5、20、90分钟,但当它们到达关键点时,可以联系人类寻求帮助、反馈或批准
  • 高风险工具 :如果你能够快速引入各种人类,你可以让智能体访问更高风险的操作,如发送外部电子邮件、更新生产数据等。保持清晰的标准可以让你对智能体进行审计并对其执行更大更好的事情充满信心

因素 12. 让你的智能体成为一个无状态的归约器

好吧,到目前为止我们已经有1000多行的markdown了。这一点主要是为了好玩。

picture.image

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
火山引擎大规模机器学习平台架构设计与应用实践
围绕数据加速、模型分布式训练框架建设、大规模异构集群调度、模型开发过程标准化等AI工程化实践,全面分享如何以开发者的极致体验为核心,进行机器学习平台的设计与实现。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论