读完文章之后我提了个issue上去,https://github.com/humanlayer/12-factor-agents/issues/63
我理解我们可以为Agent界定东南西北四个边界。
北:Agent如何与外部互动
南:Agent如何与大模型互动
西:上下文管理
东:工具调用管理
对设计Agent而言,我们要做的是从北走到南,再回来。考虑到这是一个数字系统,结构化的数据就成为了我们的通行证。
以下是原文翻译和节选。
免责声明:
我不确定说这话的确切位置,但这里似乎和任何地方一样好:这绝不是对现有许多框架的挖掘,也不是对从事这些框架工作的非常聪明的人的挖掘。它们实现了不可思议的事情,并加速了人工智能生态系统的发展。
我希望这篇文章的一个结果是,代理框架构建者可以从自己和其他人的旅程中学习,使框架变得更好。
特别是对于那些想要快速行动但需要深度控制的建筑商来说。
免责声明2:
我不打算谈论MCP。我相信你能看出它适合哪里。
免责声明3:
出于某种原因,我主要使用打字脚本,但所有这些东西都可以用python或您喜欢的任何其他语言运行。
我们今天的软件设计以及即将讨论的Agent设计因素。
要更深入地了解我的代理之旅以及是什么让我们来到这里,请查看《软件简史》——这里有一个快速总结:
代码的主要工作模型,有向图
我们将深入探讨有向图(DG)及其非循环伙伴DAG。我将首先指出这一点。…好吧。…软件是有向图。我们过去用流程图来表示程序是有原因的。
从代码到有向图
大约20年前,我们开始看到DAG编排变得流行起来。我们谈论的是像Airflow、Prefect这样的经典,一些前辈,还有一些新的经典,比如(dagster、inggest、风车)。这些遵循相同的图形模式,并具有可观察性、模块化、重试、管理等额外好处。
当然,10~15年前开始我们有了机器学习介入这个DAG,使得事情变成了这个样子。
Agent对有向图编排的冲击
我不是第一个这么说的人,但当我开始学习代理时,我最大的收获是你可以扔掉DAG。您可以给代理一个目标和一组转换,而不是软件工程师对每个步骤和边缘情况进行编码,让大模型做出决定,而不是给定路线。
这里发生的事情是,你编写的软件更少,你只需给LLM图的“边”,让它找出节点。您可以从错误中恢复,可以编写更少的代码,您可能会发现LLM为问题找到了新的解决方案。
Agent循环
正如我们稍后将看到的,事实证明这不太奏效。
让我们再深入一步——使用Agent,你会得到一个由3个步骤组成的循环:
- LLM决定工作流的下一步,输出结构化json(“工具调用”)
- 确定性代码执行工具调用
- 结果将附加到上下文窗口
- 重复,直到确定下一步“完成”
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)
我们的初始上下文只是开始事件(可能是用户消息,可能是启动的cron,可能是webhook等),我们要求llm选择下一步(工具)或确定我们已经完成。
这种方法的问题
- Agent会丢失上下文,他们一次又一次地尝试同样的失败方法
即使您还没有手动生成代理,您也可能在使用代理编码工具时看到过这个长期的上下文问题。他们只是在一段时间后迷路了,你需要开始新的聊天。
我甚至可能会假设一些我偶尔听到的东西,你可能已经形成了自己的直觉:
即使模型支持越来越长的上下文窗口,您也总是可以通过一个小而专注的提示和上下文获得更好的结果
我交谈过的大多数构建者都把“工具调用循环”的想法推到了一边,因为他们意识到任何超过10-20圈的事情都会变成LLM无法恢复的大混乱。即使代理商90%的时间都是正确的,那也离“足够好,可以交给客户”还有很长的路要走。你能想象一个网页应用程序在10%的页面加载时崩溃吗?
实际有效的方法 - micro Agent
我在看到的一件事是,将Agent模式分散到更广泛、更确定的DAG中。
你可能会问——“在这种情况下为什么要使用Agent?”——我们很快就会讨论这个问题,但基本上,让语言模型管理范围良好的任务集可以很容易地整合实时人类反馈,将其转化为工作流程步骤,而不会陷入上下文错误循环。(因子1、因子3、因子7)。
让语言模型管理范围良好的任务集,可以很容易地整合实时的人类反馈。…而不会陷入上下文错误循环
一个真实的例子
下面是一个示例,说明确定性代码如何运行一个micro Agent,负责处理部署的人在循环步骤。
- Human 将PR合并到GitHub主分支
- 确定性代码 部署到临时环境
- 确定性代码 针对分段运行端到端(e2e)测试
- 确定性代码 将产品部署交给代理,初始上下文为:“将SHA 4af9ec0部署到生产环境”
- Agent 调用
deploy_front_to_prod(4af9ec0)
- 确定性代码 请求人工批准此操作
- 人类 拒绝该操作,并反馈“你能先部署后端吗?”
- Agent 调用
deploy_backend_to_prod(4af9ec0)
- 确定性代码 请求人工批准此操作
- 人类 批准该行为
- 确定性代码 执行了后端部署
- Agent 调用
deploy_front_to_prod(4af9ec0)
- 确定性代码 请求人工批准此操作
- 人类 批准该行为
- 确定性代码 执行了前端部署
- Agent 确定任务已成功完成,我们完成了!
- 确定性代码 对生产环境运行端到端测试
- 确定性代码 任务已完成,或传递给回滚Agent以检查失败并可能回滚
这个例子基于我们用于管理我们自己部署的开源Agent
我们还没有给这个代理一大堆工具或任务。LLM的主要价值是解析人类的明文反馈,并提出更新的行动方案。我们尽可能地隔离任务和上下文,以使LLM专注于一个5-10步的小工作流程。
在看过micro Agent之后,什么是Agent?
实际上这里和Google AI Agent白皮书很相似,也就是业务编排。
- 提示词 - 告诉LLM如何表现,以及它有什么“工具”可用。提示的输出是一个JSON对象,描述了工作流中的下一步(“工具调用”或“函数调用”),factor 2。
- switch语句 - 基于LLM返回的JSON,决定如何处理它,factor 8的一部分。
- 累积上下文 - 存储已发生的步骤及其结果列表,factor 3。
- for循环 - 在LLM发出某种“终端”工具调用(或明文响应)之前,将switch语句的结果添加到上下文窗口中,并要求LLM选择下一步,factor 8。
在“micro Agent”示例中,我们从拥有控制流和上下文累积中获得了一些好处:
- 在我们的switch语句和for循环中,我们可以劫持控制流以暂停人工输入或等待长时间运行的任务完成
- 我们可以轻松地序列化暂停+恢复的上下文窗口
- 在我们的提示符中,我们可以优化如何向LLM传递指令和“到目前为止发生了什么”
为什么要有12-factor agents?
最终,这种方法并没有达到我们想要的效果。
在构建HumanLayer的过程中,我与至少100位SaaS构建者(主要是技术创始人)进行了交谈,他们希望使现有产品更具代理性。旅程通常是这样的:
- 决定你想建立一个代理
- 产品设计、用户体验映射、需要解决的问题
- 想要快速行动,所以拿上$FRAMEWORK开始建设
- 达到70-80%的质量标准
- 意识到80%对于大多数面向客户的功能来说还不够好
- 要意识到,要超过80%,需要对框架、提示、流程等进行逆向工程。
- 从头开始
优秀LLM应用程序的设计模式
在阅读了数百本人工智能书籍并与数十位创始人合作后,我的直觉是:
- 有一些核心因素使Agent变得伟大
- 全力以赴地建立一个框架,并构建一个基本上是绿地重写的东西,可能会适得其反
- 有一些核心原则使代