Hi,大家好,我叫秋水,当前专注于 AI Agent(智能体)。
最近AI 智能体开发领域有一篇技术指南引起了很大的反响——《12-Factor Agents》,github上已经6.3k star了。
它提出了构建可靠的AI智能体的12个核心原则。这些原则精准地击中了我们在实际AI项目中遇到的核心痛点:
• 为什么 智能体 在长对话中总是"转圈圈",重复执行同样的错误操作?
• 如何让 智能体 在执行核心业务能够暂停并等待人工审批?
• 为什么现有的Agent框架很难达到生产环境的质量要求?
• 怎样构建既能自主决策又能在关键时刻寻求人工帮助的 智能体 ?
• 如何让 智能体 真正融入到现有的业务系统?
今天和大家分享一下这12个原则。
为什么需要12-Factor Agents?
在深入这12个原则之前,我们需要理解一个核心认知转变:
真正有效的智能体不是"给一堆工具,让其循环执行,直到完成目标"的模式。
作者Dex通过与上百个技术创始人的交流发现,大多数生产环境中的"AI智能体"实际上是:
主要由传统代码构成,在关键节点巧妙融入LLM步骤的混合系统。
软件架构的演进历程
让我们回顾一下软件架构的发展:
60年前:软件就是有向图(DAG),我们用流程图表示程序逻辑
20年前:出现了Airflow、Prefect等工作流管理工具,提供了可观测性、模块化、重试、管理等能力
10-15年前:机器学习模型开始融入工作流,比如"将文本总结"或"按严重程度分类"
现在:大语言模型带来了新的可能性——不用预编写每个步骤,而是给智能体目标和工具,让LLM实时决定执行路径
"循环直到解决"模式的致命问题
传统的智能体循环包含3个步骤:
1. LLM确定下一步,输出结构化JSON
2. 传统代码执行函数调用
3. 结果追加到会话上下文
4. 重复直到确定"完成"
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)但是这种模式最大的问题是:当会话上下文变长时,智能体会迷糊,不断重复错误方法。
真正有效的方案:专业化智能体
实际有效的模式是:将智能体融入到主要由传统代码构成的系统工作流中。
12个核心原则 第1原则:自然语言转工具调用
核心思想:将自然语言转换为结构化工具调用,这是智能体最基础但也是最重要的能力。
例如:
用户输入:"能为赞助二月份AI聚会的Terri创建一个750美元的付款链接吗?"
智能体输出:
{ "function":{ "name":"create_payment_link", "parameters":{ "amount":750, "customer":"cust_128934ddasf9", "product":"prod_8675309", "price":"prc_09874329fds", "quantity":1, "memo":"二月份AI聚会赞助费用" } } }技术实现:
# LLM将自然语言转换为结构化对象 nextStep = await llm.determineNextStep( "为赞助二月份AI聚会的Jeff创建750美元付款链接" ) # 基于结构化输出处理 if nextStep.function == 'create_payment_link': stripe.paymentlinks.create(nextStep.parameters) return elif nextStep.function == 'something_else': pass else: # 模型调用了未知函数 pass这个原则看似简单,实际上是整个智能体架构的基石。它的深层价值在于创建了一个语言与行动之间的桥梁。
传统的软件需要用户学习特定的操作界面和命令,而这个原则让用户可以用自然语言描述意图,由智能体理解并转换为工具调用。
更重要的是,这种转换不是简单的关键词匹配,而是基于语义理解的结构化输出。
LLM能够理解上下文,推断出用户没有明确说出的信息(比如从"Terri"推断出对应的customer_id),这是传统自然语言处理技术难以达到的。
这个原则的技术难点在于如何确保输出的结构化数据既准确又完整。在实际应用中,我们需要精心设计提示词来引导LLM输出符合预期格式的JSON,同时处理各种边界情况和异常输入。
第2原则:完全掌控你的提示词
核心思想:不要把提示词工程交给开发框架,例如Agent框架CrewAI,提示词是你与LLM的主要接口,必须完全掌控。
示例(框架黑盒模式):
agent = Agent( role="部署助手", goal="安全管理部署", personality="谨慎细致", tools=[tool1, tool2, tool3] )这种方式让你无法精确控制传给LLM的具体指令。
推荐做法(完全掌控):
function DetermineNextStep(thread: string) -> DoneForNow | ListGitTags | DeployBackend { prompt = """ 你是一个管理前后端系统部署的助手。 你致力于遵循最佳实践,确保安全成功的部署。 在部署任何系统前,你应该检查: - 部署环境(测试vs生产) - 正确的标签/版本 - 当前系统状态 对于敏感部署,使用request_approval获取人工确认。 优先考虑以下步骤: - 检查当前部署状态 - 验证部署标签存在 - 如需要则请求审批 - 先部署到测试环境再到生产环境 - 监控部署进度 当前情况: {thread} 下一步应该做什么? """ }这个原则体现了开发智能体的一个矛盾点:效率与控制。
技术框架能让我们快速上手,但当我们需要精准调优时,框架的封装使得我们无法触达底层。
提示词是我们与LLM沟通的唯一语言,它直接决定了输出的质量。
当我们把这个核心控制权交给框架时,我们就失去了对最关键部分的控制。
更深层的洞察是:每个业务场景都有其独特性。通用的提示词模板无法涵盖所有的业务细节、异常情况和特殊需求。只有完全掌控提示词,你才能:
1.精确调优:根据实际反馈精确调整每一个词汇
2.业务适配:加入特定的业务规则和约束
3.性能优化:优化API调用次数,提高响应速度
4.安全控制:确保智能体不会越界执行越界操作
这个原则的本质是要求开发者把提示词当作代码来管理——版本控制、测试、文档化、代码审查。
第3原则:完全掌控你的会话上下文
核心思想:不必使用标准消息格式传递上下文,要针对你的场景优化上下文结构。
传统标准格式:
[ { "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\": [...]}", "tool_call_id":"1" } ]自定义优化格式:
From: @alex Channel: #deployments Text: 能部署最新后端到生产环境吗? slack_message>
intent: "list_git_tags" list_git_tags>
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>
技术实现:
class Thread: events: List[Event] classEvent: type: str# "list_git_tags", "deploy_backend", "error"等 data: Any# 对应的数据 defevent_to_prompt(event: Event) -> str: data = event.data ifisinstance(event.data, str) \ else stringify_to_yaml(event.data) returnf"<{event.type}>\n{data}\n {event.type}>" defthread_to_prompt(thread: Thread) -> str: return'\n\n'.join(event_to_prompt(event) for event in thread.events)这个原则挑战了我们对API标准化的固有认知。OpenAI等厂商提供的标准消息格式看似是"最佳实践",但实际上它们是为通用场景设计的方案。
上下文工程的本质是信息架构设计,就像数据库设计一样,不同的业务场景需要不同的信息组织方式,标准格式往往存在以下问题:
1.信息冗余:很多元数据(如role、tool_call_id)对实际推理没有帮助
2.结构僵化:无法灵活表达复杂的业务关系
3.API调用浪费:JSON格式的冗余字符消耗成本高昂的API调用次数
4.语义模糊:标准格式无法传递业务特定的语义信息
通过自定义上下文格式,你获得了几个关键优势:
信息密度优化:同样的API调用能传递更多有效信息。比如用XML标签可以清晰表达信息层次,用YAML可以减少冗余字符。
语义增强:可以在格式中编码业务逻辑。比如用
标签不仅表示这是错误信息,还隐含地告诉LLM需要错误修复。
注意力引导:通过结构化的信息布局,引导LLM关注最重要的信息。比如把最新的事件放在最显眼的位置。
更深层的洞察是:LLM对信息结构非常敏感。相同的信息用不同方式组织,会得到截然不同的输出质量。这就像人类阅读一样,好的排版和结构能显著提高理解效率。
这个原则的终极目标是:让每一次API调用都承载最大的语义价值。
第4原则:工具(函数)调用就是结构化输出
很多人都认为工具调用是一个复杂的过程:LLM 需要理解工具的功能,然后"调用"这个工具,工具执行后返回结果。
但实际上,工具调用的本质就是让 LLM 输出一个结构化的 JSON,然后我们的代码根据这个 JSON 来执行对应的操作。
这就像是:
•传统理解:AI Agent 直接操作工具,就像人拿起锤子敲钉子
•实际情况:AI Agent 只是写了一张"请用锤子敲钉子"的纸条,真正敲钉子的是我们的代码
具体是怎么实现的?
假设我们有两个工具:创建工单和搜索工单。
class Issue: title: str description: str team_id: str assignee_id: str classCreateIssue: intent: "create_issue" issue: Issue classSearchIssues: intent: "search_issues" query: str what_youre_looking_for: str传统思路:定义两个函数create_issue()和search_issues(),让 LLM 选择调用哪个。
新思路:让 LLM 输出一个 JSON,我们根据 JSON 的intent字段来决定执行什么操作。
# LLM 输出的 JSON 可能是这样的: { "intent": "create_issue", "issue": { "title": "用户无法登录", "description": "用户反馈登录页面一直转圈", "team_id": "tech_support", "assignee_id": "zhang_san" } }然后我们的代码这样处理:
if next_step.intent == 'create_issue': # 调用真正的工单系统 API jira_api.create_issue(next_step.issue) return "工单创建成功" elif next_step.intent == 'search_issues': # 调用搜索 API results = jira_api.search(next_step.query) return f"找到 {len(results)} 个相关工单" else: return "无法识别的操作"这样做有什么好处?
1.控制权在你手里
LLM 只负责"决策",具体"执行"由你的代码控制。这意味着:
• 你可以决定是否真的执行这个操作
• 你可以添加权限检查、参数验证等
• 你可以根据上下文动态调整执行逻辑
2.更大的灵活性
不是每个"工具调用"都要对应一个固定的函数。比如:
• 同样是"发送邮件",VIP 客户和普通客户可能走不同的发送逻辑
• 同样是"查询数据",不同的用户角色可能查询不同的数据源
3.更容易调试和监控
因为所有的决策都以 JSON 形式明确表达,你可以:
• 记录每次 LLM 的决策过程
• 分析哪些工具被使用最多
• 快速定位问题出在决策还是执行环节
第5原则:统一执行状态和业务状态
核心思想:不要分离"执行状态"和"业务状态",尽可能统一管理。
很多基础设施系统习惯把这两种状态分开管理,设计复杂的抽象来跟踪各种状态信息。
执行状态:系统当前在做什么
• 当前步骤
• 下一步要做什么
• 等待状态
• 重试次数等
业务状态:系统到目前为止发生了什么
• OpenAI消息列表
• 工具调用记录和结果
• 整个工作流程的历史
实际上,我们可以让智能体从上下文窗口中推断出所有执行状态。因为执行状态往往只是对"已经发生什么事情"的描述。
举个例子,不需要单独维护"当前在等待用户输入订单号"这样的执行状态,AI看到对话中最后一句是"请提供您的订单号",自然就知道现在在等什么。
如果可能,尽量简化并统一状态管理。
不要为了分离而分离。对于AI Agent来说,让AI从丰富的上下文中推断应该做什么,往往比维护复杂的状态系统更简单、更可靠。
记住:选择最适合你应用场景的方案,而不是盲目遵循传统的状态分离模式。
统一管理的好处:
1.简洁性- 所有状态信息来自同一个地方
2.序列化- 整个对话线程很容易保存和加载
3.调试- 所有历史记录集中查看,问题一目了然
4.灵活性- 添加新功能只需增加新的事件类型
5.恢复- 系统崩溃后可以从任何节点恢复
6.分叉- 可以在任意时间点创建对话分支
7.人性化- 容易转换成可读的文档或者web界面
核心思想:智能体应该支持启动、暂停、恢复操作。
关键特性:能够在函数选择和函数执行之间暂停,这对需要人工审核的核心业务操作至关重要。
大多数框架将智能体开发为同步的、一次性执行。一旦执行,就必须运行到完成或失败。这种模式忽略了现实业务场景的复杂性:
1.人机协作的异步性:人类的响应时间是不可预测的,可能是几分钟,也可能是几小时
2.外部依赖的不确定性:很多操作需要等待外部系统的响应
3.风险控制的必要性:核心业务操作需要在执行前暂停进行人工审核
暂停/恢复机制:
细粒度控制:能够在任意时刻暂停,特别是在"函数选择"和"函数执行"之间暂停,这是风险控制的关键节点。
资源效率:长时间等待期间不占用计算资源,系统可以处理其他任务。
故障恢复:如果系统在等待期间重启,可以无缝恢复到暂停前的状态。
多模式交互:支持同步和异步两种交互模式,适应不同的使用场景。
第7原则:用函数调用与人类交互
在传统的AI Agent实现中,存在一个关键的决策点:LLM 需要在每次响应时做出选择——返回纯文本内容还是结构化数据?
比如当用户问"东京的天气如何"时:
• 如果返回纯文本,第一个token可能是"东"
• 如果调用工具,第一个token是JSON对象的开始标记
这种设计让AI Agent在交互的灵活性上受到很大限制。
与其让AI Agent在纯文本和结构化数据之间纠结,不如让它始终输出JSON格式,然后通过自然语言标记来表达意图。
这就像是给AI Agent制定了一套"内部语言",让它能够更清晰地表达自己的想法和需求。
这样的好处是:
1. 指令更加清晰
不同类型的人类交互工具让LLM能够给出更具体、更精准的回应。
2. 内外循环灵活切换
这是最重要的一点:传统的AI Agent都是被动响应(Human->Agent),而这种设计支持主动发起(Agent->Human)。
想象一下:你的AI Agent可以在每天早上9点主动给你发送昨日数据分析报告,或者在检测到异常时主动联系相关人员。
3. 多人协作支持
通过结构化事件,可以轻松追踪和协调不同人员的输入。比如在审批流程中,AI Agent可以同时联系多个审批人。
4. 多智能体扩展
这套抽象设计可以很容易扩展为Agent->Agent的请求和响应。想象一下多个AI Agent之间的协作场景。
5. 持久性和可靠性
结合状态管理,整个工作流变得持久、可靠且易于监控。即使系统重启,也能从上次的状态继续执行。
第8原则:完全掌控你的工作流
核心思想:构建适合特定场景的工作流,不同类型的工具调用可能需要不同的工作流程处理。
这个原则涉及智能体架构的最核心问题:谁来控制执行流程?
大多数框架选择了"LLM控制一切"的模式,但这种选择隐含着很大的风险和限制。
1.LLM并不擅长控制流程:LLM是为内容生成而设计的,而不是为流程控制。让它同时负责"决定做什么"和"如何控制执行"是发生混乱。
2.缺乏业务逻辑的表达能力:复杂的业务规则(如审批流程、权限控制、异常处理)很难仅通过提示词来精确表达。
3.不可预测的执行路径:当LLM控制流程时,你无法确保系统按照预期的路径执行,这在生产环境中是不可接受的。
自定义工作流的重要性:
精确的业务逻辑表达:通过代码而不是提示词来表达业务规则,可以实现精确的条件判断、循环、异常处理等复杂逻辑。
可预测的执行行为:关键的控制决策由代码实现,确保系统行为的可预测性。
细粒度的资源控制:可以精确控制何时消耗计算资源、何时进行网络调用。
灵活的集成能力:可以轻松集成现有的业务系统、工作流引擎、监控系统等。
因此智能体系统应该采用混合控制模式:
• LLM负责"智能决策"(what to do)
• 代码或者原先系统负责"流程控制"(how to do it)
这种分离带来了几个重要好处:
可测试性:控制流程逻辑可以独立于LLM进行单元测试。
可调试性:可以精确追踪执行路径,定位问题根源。
可扩展性:可以轻松添加新的控制逻辑,而不需要重新训练或调整LLM。
第9原则:将错误存到会话上下文
核心思想:利用智能体的"自我修复"能力,让LLM读取错误信息并在后续工具调用中修正问题。
基础实现:
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) thread["events"].append({ "type": next_step.intent + '_result', "data": result, }) except Exception as e: # 错误添加到会话上下文,让LLM尝试修复 thread["events"].append({ "type": 'error', "data": format_error(e), }) # 循环重试错误计数和限制:
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这个原则展现了AI系统相对于传统软件的一个独特优势:错误理解和自适应修复能力。
但更重要的是,它揭示了错误处理在AI系统中的全新范式。
传统错误处理的局限性:
传统软件中,错误处理主要依赖于:
1.预定义的异常类型:程序员预先考虑可能的错误情况
2.静态的处理逻辑:每种错误对应固定的处理方式
3.人工干预:复杂错误需要人工分析和修复
这种模式在面对LLM系统时显得力不从心,因为LLM可能产生各种意料之外的错误。
AI错误处理的新范式:
语义错误理解:LLM不仅能识别错误的类型,还能理解错误的语义含义,比如"权限不足"意味着需要更高权限的凭证。
上下文相关的修复策略:同样的错误在不同上下文中可能需要不同的修复方法,LLM能够根据具体情况选择合适的策略。
增量学习能力:每次错误处理都会增加系统的"经验",虽然单次会话中LLM不会记住,但可以通过上下文累积来改善处理策略。
错误处理的分层策略:
1.自动重试层:LLM尝试理解和修复错误
2.上下文重构层:如果重试失败,重新组织上下文信息
3.人工干预层:如果自动修复失败,升级给人类处理
4.系统级回退层:如果人工干预也无效,回退到安全状态
这种分层策略确保了系统既能充分利用AI的自我修复能力,又不会因为过度依赖而失控。
最重要的是:错误不再是系统的异常状态,而是学习和改进的机会。这种思维转变对于构建真正智能体系统至关重要。
第10原则:专业化智能体
核心思想:构建小而专注、做好一件事的智能体,而不是试图做所有事情的单体智体手。
核心洞察:任务越大越复杂,需要的步骤就越多,意味着更长的会话上下文,随着上下文增长,LLM更容易迷糊。
推荐范围:保持智能体专注于特定领域的3-10个步骤,最多20个步骤,确保会话上下文可管理,LLM性能高。
这个原则挑战了软件工程中的一个常见误区:大而全的系统更强大。在传统软件开发中,我们确实倾向于构建功能丰富的大型系统,但在AI智能助手领域,这种思路是有害的。
认知负荷理论在AI中的应用:
人类认知科学研究表明,人类的工作记忆有限,一次只能处理7±2个信息单元。LLM虽然没有相同的认知限制,但存在类似的注意力分散问题:
1.上下文稀释:信息量越大,每个信息单元获得的"注意力"越少
2.目标漂移:长任务中容易偏离原始目标
3.决策疲劳:连续的决策制定会降低决策质量
专业化智能体的深层优势:
注意力集中:有限的任务范围让LLM能够集中所有"注意力"在关键决策上。
专业化优势:每个智能体可以针对特定领域进行优化,就像人类的专业分工一样。
故障隔离:一个智能助手的故障不会影响整个系统的其他部分。
测试简化:小智能体的行为空间有限,更容易进行全面测试。
智能体网络的新架构模式:
专业化小智能体原则实际上指向了一种新的系统架构:智能体网络,而不是单一的超级智能体。
这类似于微服务架构相对于单体应用的优势:
•独立部署:每个智能助手可以独立更新
•技术多样性:不同智能助手可以使用最适合的技术栈
•水平扩展:可以根据需求扩展特定功能的智能体
•团队分工:不同团队可以负责不同的智能体
智能不等于复杂。真正的智能体现在能够精确、可靠地完成特定任务,而不是试图解决所有问题。
第11原则:多渠道触发,用户在哪里就在哪里服务
核心思想:让用户能够从Slack、邮件、短信等任何渠道触发智能体,智能体也能通过相同渠道响应。
这个原则解决了AI应用面临的一个根本性用户体验问题:上下文切换的认知成本。
用户体验的深层洞察:
当用户需要使用AI智能体时,传统方式要求他们:
1. 停止当前工作
2. 打开专门的AI智能体
3. 输入请求
4. 等待响应
5. 将结果带回到原工作环境
这种上下文切换造成了巨大的认知成本和工作流程中断。
优势:
•在用户习惯的地方找到用户:让AI应用能够像真人一样自然交流,或者至少像靠谱的数字同事一样工作
•AI主动工作模式:AI不需要人来启动,可以被系统事件、定时任务、故障报警等自动触发。AI可能会工作5分钟、20分钟或90分钟,但遇到重要决策时,会主动联系人类请求帮助、获取反馈或寻求批准
•处理重要任务的能力:当你能快速让合适的人参与进来时,就可以放心让AI处理重要操作,比如发送对外邮件、更新生产环境数据等。有了清晰的操作规范,你就能追踪AI的行为,对AI处理重大任务更放心
总结与思考
这12个原则不是孤立的,而是相互联系的。它们共同指向一个核心理念:在AI的不确定性之上构建确定性的系统。
核心思想的三个层次:
1.控制权层次:完全掌控提示词、会话上下文、控制流程等关键组件
2.架构层次:采用专业化智能体、状态统一、可暂停恢复的设计
3.交互层次:支持多渠道、人机协作、主动式的交互模式
不要试图构建万能的AI智能体,而要构建专业的AI同事。每个智能体都应该在特定领域内表现卓越,需要帮助时能够寻求协作。
这些原则,它们不仅解决了我在AI项目中遇到的具体问题,更重要的是,它们提供了一个系统性的思考框架,帮助我们在AI的浪潮中找到可靠的技术立足点。
今天的文章就到这里结束了,如果你觉得不错,随手点个赞、在看、转发三连吧。
如果想第一时间收到推送,也可以给我个星标⭐。
谢谢你看我的文章,我们,下次再见。
今天的文章就到这里结束了,如果你觉得不错,随手点个赞、在看、转发三连吧。
如果想第一时间收到推送,也可以给我个星标⭐。
谢谢你看我的文章,我们,下次再见。
联系/社群
相关视频:
热门跟贴