
从理论到实践:我用12要素方法重构了AI 智能体,效果惊人! 原创 精华
本文为 12-Factor Agents[1] 内容的翻译整理,并结合个人实践补充了部分笔记和理解。内容涵盖 LLM 代理开发的 12 项核心原则。
“12 要素代理” 框架受著名的 12 要素应用方法论启发,为构建可靠、可维护且可扩展的 LLM 应用提供了全面的指南。这些原则解决了将非确定性 AI 组件集成到生产系统的独特挑战,强调明确的控制、托管上下文以及无缝的人机协作。
1. 自然语言工具调用
这是Agent架构中最基础但也最关键的转换层。当用户说"帮我创建一个5000元的付款链接给琳达,用于赞助三月AI技术沙龙"时,系统需要将这句话精确转换为结构化的API调用。以下是一个真实的转换示例:
输入的自然语言:
"帮我创建一个5000元的付款链接给琳达,用于赞助三月AI技术沙龙"
转换后的结构化工具调用:
{
"function":{
"name":"create_payment_link",
"parameters":{
"amount":5000,
"customer":"cust_128934ddasf9",
"product":"prod_8675309",
"price":"prc_09874329fds",
"quantity":1,
"memo":"这是三月AI技术沙龙的赞助付款链接"
}
}
}
随后,确定性代码可以获取有效载荷并进行处理。
# LLM 接收自然语言并返回结构化对象
nextStep = await llm.determineNextStep(
"""
create a payment link for $750 to Jeff
for sponsoring the february AI tinkerers meetup
"""
)
# 根据结构化输出的 function 字段进行处理
ifnextStep.function == 'create_payment_link':
stripe.paymentlinks.create(nextStep.parameters)
return # 或者你想要的其他处理,见下文
elifnextStep.function == 'something_else':
# ... 其他情况
pass
else: # themodeldidn'tcallatoolweknowabout
# 模型未调用已知工具,执行其他操作
pass
必须深刻理解这一转换过程的每个环节:从自然语言解析到实体识别,再到参数映射和 API 构造。切勿将其视为黑盒,因为任何细节的偏差都可能导致用户意图被误解,进而影响系统的可靠性和用户体验。
理解笔记
构建 Agent 过程中,工具方面,需要做的内容是:
- 工具(Tool)说明文档要结构化、清晰,确保 LLM 能准确理解每个工具的用途、参数和限制,减少歧义。
- 需要为 LLM 返回的结构化调用结果设计健壮的处理流程,包括成功、失败、异常等分支,保证系统的可预测性和安全性。
- 工具接口和文档应与代码保持同步,避免文档不同步,导致 LLM 理解偏差。
2. 拥有自己的提示词
提示词是 LLM 交互的核心,决定了交互的行为和输出。本原则强调应将提示词视为关键、受版本控制的代码资产,而非一次性输入。提示应纳入代码库统一管理,便于系统性测试、部署和回滚。掌控自己的提示词有助于保证一致性、可复现性,并能以可控方式持续优化代理行为。
应将你的提示词视为一等公民的代码:
function DetermineNextStep(thread: string) -> DoneForNow | ListGitTags | DeployBackend | DeployFrontend | RequestMoreInformation {
prompt #"
{{ _.role("system") }}
你是一个负责前端和后端系统部署的高效助手。
你会遵循最佳实践,确保部署安全且成功。
并遵循正确的部署流程。
在部署任何系统前,你应检查:
部署环境(测试/生产)
部署的正确标签/版本
当前系统状态
你可以使用 deploy_backend、deploy_frontend、check_deployment_status 等工具
管理部署。对于敏感部署,使用 request_approval 获取人工确认。
human verification.
始终先思考下一步该做什么,比如:
检查当前部署状态
校验部署标签是否存在
如有需要,申请人工批准
先部署到测试环境再到生产环境
监控部署进度
{{ _.role("user") }}
{{ thread }}
下一步应该做什么?
"#
}
拥有自己的提示词的主要好处:
- 完全控制:可精确编写代理所需指令,无需依赖黑盒抽象
- 测试和评估:可像测试其他代码一样测试和评估提示
- 快速迭代:可根据实际表现快速修改提示
- 透明度:清楚了解代理正在执行的指令
- 角色技巧:可利用支持非标准用户/助手角色的 API
请记住:提示词是你的应用逻辑与 LLM 之间的主要接口。
完全掌控提示词,为生产级代理提供了必要的灵活性和提示控制。
我无法断言什么是最优提示,但你一定需要灵活性以便不断尝试。
理解笔记
构建 Agent 过程中,提示词需要自主可控。提示(Prompt)应作为代码资产进行版本管理和测试,确保每次变更都可追溯和回滚。
- 推荐采用结构化、可维护的提示工程工具,提升提示的可读性和可复用性。
- 保持提示的灵活性和可控性,有助于快速迭代和适应业务需求变化。
3. 拥有自己的上下文窗口
上下文窗口是 LLM 对过往交互的有限记忆。本原则主张主动且有策略地管理上下文,确保只保留与当前任务直接相关的信息,及时剔除无关内容,防止"上下文漂移"或信息冗余。高效的上下文管理不仅能提升 LLM 的性能,还能减少 token 消耗,让模型聚焦于关键细节。
无需总是采用标准消息格式向 LLM 传递上下文。
标准消息格式 vs 自定义格式对比:
标准消息格式:
[
{
"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"
}
]
优化的自定义格式(更高效):
[
{
"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?
}
]
实现上下文构建的Python代码:
class Thread:
events: List[Event]
classEvent:
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: Union[ListGitTags, DeployBackend, DeployFrontend, RequestMoreInformation,
ListGitTagsResult, DeployBackendResult, DeployFrontendResult,
RequestMoreInformationResult, str]
defevent_to_prompt(event: Event) -> str:
data = event.data ifisinstance(event.data, str) \
else stringifyToYaml(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)
不同阶段的上下文窗口示例:
初始钉钉群请求:
<dingtalk_message>
From: @小张
Channel: #部署群
Text: 可以帮我部署最新的后端到生产环境吗?
</dingtalk_message>
错误处理和恢复后:
<slack_message>
From: @alex
Channel: #deployments
Text: Can you deploy the latest backend to production?
</slack_message>
<deploy_backend>
intent: "deploy_backend"
tag: "v1.2.3"
environment: "production"
</deploy_backend>
<e>
error running deploy_backend: Failed to connect to deployment service
</e>
<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>
关键好处包括:
- 信息密度:以最大化LLM理解的方式构建信息
- 错误处理:以帮助LLM恢复的格式包含错误信息
- 安全性:控制传递给LLM的信息,过滤敏感数据
- 灵活性:随着您了解最佳实践而调整格式
- Token效率:优化上下文格式以提高token效率和LLM理解
在任何时刻,你给代理 LLM 的输入都是"这是目前为止发生的事情,下一步是什么?"
一切都关乎上下文工程。LLM 是无状态函数,只负责将输入转化为输出。要获得最佳输出,必须为其提供最优输入。
营造良好上下文环境包括:
- 明确的提示和指令
- 检索到的文档或外部数据(如 RAG)
- 过往状态、工具调用、结果或其他历史
- 相关但独立的历史/对话信息(记忆)
- 输出结构化数据类型的说明
理解笔记
构建 Agent 过程中,为了使 LLM 拥有更好的效果,从提示词工程向情景工程转换,利用工程方法和系统思维,提供 LLM 所需的:
- 当前情况:当前背景、当前进展;
- 目标任务:分解目标、最终目标;
- 已有工具、手段;
- 已有信息:历史资料、过往记忆、过往案例;
上下文管理不仅仅是拼接历史消息,更要有选择性地组织和压缩信息,确保 LLM 只接收到与当前任务最相关的内容。
建议建立上下文构建的标准流程和模板,便于团队协作和复用。
4. 工具只是结构化的输出
工具调用被过度神秘化了,其实质就是LLM输出的结构化JSON数据,您的代码解析这些数据并执行相应操作。让我们看一个具体的例子,假设您有两个工具CreateIssue
和SearchIssues
,让 LLM "在几种工具中选择一种"其实就是让它输出 JSON,我们可以将其解析为对应的对象。
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
模式很简单:
- LLM 输出结构化 JSON
- 确定性代码执行相应操作(如调用外部 API)
- 捕获结果并反馈到上下文
这样就能清晰区分 LLM 的决策和应用操作。LLM 决定做什么,代码决定如何执行。LLM 所谓"工具"并不意味着每次都要严格映射到某个函数。
理解这一本质后,你可以更灵活地设计工具接口:定义清晰数据结构、处理异常、优化性能,甚至支持非原子复杂操作。不要被"function calling"等术语迷惑,工具调用只是 LLM 决策与应用逻辑之间的桥梁,关键是保持决策层和执行层的清晰分离。
重要的是,"下一步"未必像"运行纯函数并返回结果"那样原子。当你把"工具调用"视为模型输出、描述确定性代码应做什么的 JSON 时,就获得了极大灵活性。这与原则八(拥有控制流)完美结合。
理解笔记
构建 Agent 过程中,采用关注点分离方式,设计工具使用方式:
- LLM 进行决策,分析当前情景,决定下一步要干什么;
- 工具进行确定性执行;
- 工具调用的本质是 LLM 输出结构化数据(如 JSON),由后端代码解析并执行,LLM 不直接操作外部世界。
- 关注点分离有助于系统的可维护性和安全性,LLM 负责"决策",代码负责"执行"。
5.统一执行状态和业务状态
传统系统往往分离执行状态(当前步骤、等待状态、重试次数等)和业务状态(用户数据、处理历史等),这种分离会增加系统复杂度并带来一致性问题。在AI应用之外的许多基础设施系统也试图分离"执行状态"和"业务状态",这种分离可能是有价值的,但对您的用例来说可能过于复杂。
概念梳理:
- 执行状态:当前步骤、下一步、等待状态、重试次数等。
- 业务状态:代理工作流程中到目前为止发生的事情(例如 OpenAI 消息列表、工具调用和结果列表等)
如果可能的话,简化——尽可能统一这些状态。实际上,您可以设计应用程序,使得所有执行状态都可以从上下文窗口推断出来。在许多情况下,执行状态(当前步骤、等待状态等)只是关于到目前为止发生了什么的元数据。
这种方法有几个好处:
- 简单:所有状态的真相来源
- 序列化:线程可以轻松序列化/反序列化
- 调试:整个历史记录在一个地方可见
- 灵活性:只需添加新的事件类型即可轻松添加新状态
- 恢复:只需加载线程即可从任何点恢复
- 分叉:可以通过将线程的某些子集复制到新的上下文/状态 ID 中来随时分叉线程
- 人机界面和可观察性:将线程转换为人类可读的 Markdown 或丰富的 Web 应用程序 UI
理解笔记
构建 Agent 过程中,构建上下文过程中,将执行状态和业务状态进行统一,一定程度上想起来规则引擎和状态机,构建规则,然后交给事件或者数据。
- 明确区分并统一 LLM 的"执行状态"与应用的"业务状态",避免信息不一致导致的误判。
- 为每个状态变更建立可追溯日志,提升系统可观测性和可维护性。
6. 使用简单的 API 启动/暂停/恢复
LLM 代理应设计为拥有清晰、简洁的编程接口,便于生命周期管理。这意味着应提供简单的 API 来启动新的代理实例、暂停执行,以及从特定状态恢复。这对于调试、测试和管理长期运行或复杂代理流程至关重要,使开发者能够随时介入和检查代理行为。
用户、应用、管道或其他代理都应能便捷地通过 API 启动代理。
当遇到长时间运行的操作时,代理及其协调的确定性代码应能优雅地暂停。
像 webhook 这样的外部触发器应允许代理从中断点恢复,无需与代理编排器深度集成。
核心要求:
- 简单启动:用户、应用、管道和其他 Agent 应能通过简单 API 启动 Agent
- 优雅暂停:Agent 及其编排代码应能在需要时暂停
- 外部恢复:如 webhook 等外部触发器应能让 Agent 从中断点恢复,无需深度集成
这种设计对生产环境尤为重要,为 AI 系统提供必要的安全网和控制机制,使其能处理更高价值任务。最关键的能力是:我们需要能中断正在工作的 Agent 并稍后恢复,尤其是在工具选择和调用之间。
理解笔记
构建 Agent 过程中,采用简单原则,触发后寻找上下文,开始执行后通过状态 ID 进行跟进。
- 设计简单、清晰的 API,支持 Agent 的启动、暂停和恢复,便于生命周期管理。
- 为每个 Agent 实例分配唯一状态 ID,方便追踪和恢复。
- 为长流程任务设计中断点和恢复机制,提升系统健壮性和用户体验。
7. 使用工具呼叫联系人类
虽然 LLM 功能强大,但总有需要人类判断、专业知识或干预的场景。本原则主张通过工具调用,将"人机交互"明确纳入代理功能。当 LLM 遇到模糊、不确定或需外部批准的情况时,应能触发结构化通知或上报给人类操作员,确保关键决策在需要时获得人工监督。
默认情况下,LLM API 依赖于关键的"高风险"令牌选择:我们是返回纯文本,还是结构化数据?
你对第一个标记的选择非常重视,在这种 the weather in tokyo
情况下,它是:
这
但在 fetch_weather
情况下,它是一些特殊标记,表示 JSON 对象的开始:
JSON>
让 LLM 始终输出 json,并用自然语言标记(如 request_human_input
或 done_for_now
,而不是像 check_weather_in_city
这样的具体工具)声明意图,往往能获得更好效果。
不会直接提升性能,但你应大胆实验,确保有自由尝试各种方式以获得最佳结果。
class Options:
urgency: Literal["low", "medium", "high"]
format: Literal["free_text", "yes_no", "multiple_choice"]
choices: List[str]
# 人机交互工具定义
classRequestHumanInput:
intent: "request_human_input"
question: str
context: str
options: Options
# 代理循环中的用法示例
ifnextStep.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
# 跳出循环,等待带 thread 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 - 用自定义格式化的上下文窗口,几轮后上下文可能如下:
(snipped for brevity)
<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>
这样做的好处:
- 明确指示:针对不同类型人机交互的工具让 LLM 输出更具体
- 内外循环:支持在传统 ChatGPT 风格界面之外的代理流程,控制流和上下文初始化不必总是 Agent->Human
- 多人协作:结构化事件便于跟踪和协调多方输入
- 多代理:简单抽象可扩展为 Agent->Agent 请求与响应
- 持久性:结合简单 API 启动/暂停/恢复,打造持久、可靠、可自省的多方工作流
理解笔记
构建 Agent 过程中,将"人机交互"标签,明确地加入到代理的功能。
- 明确将"请求人工输入"作为一种标准工具调用,便于 LLM 在不确定或高风险场景下主动寻求人类协助。
- 为人机交互设计结构化事件和回调机制,支持多渠道通知和响应。
- 为人工干预流程建立审计和追踪,提升系统安全性和合规性。
8. 拥有自己的控制流
尽管 LLM 具备"推理"能力,但应用的整体控制流应由确定性代码明确管理。LLM 只应引导控制流的路径,而不应成为控制流本身。这样才能保证可预测性、强健的错误处理,并让开发者为 LLM 的自主性设定清晰边界,防止意外或不良行为。
如果你拥有自己的控制流,就能做很多有趣的事情。
可以为特定用例构建专属控制结构。例如,某些工具调用可能需要跳出循环,等待人工或其他长时间任务(如训练管道)响应。你还可以实现:
- 工具调用结果的汇总或缓存
- LLM 结构化输出
- 上下文窗口压缩或其他内存管理
- 日志、追踪和指标
- 客户端速率限制
- 持久睡眠/暂停/“等待事件”
这种模式允许你根据需要中断和恢复代理流程,打造更自然的对话和工作流。
例如:我对每个 AI 框架的首要诉求是,必须能在"选择工具"与"调用工具"之间中断代理并稍后恢复。
如果没有这种粒度的可恢复性,就无法在工具调用前进行人工审核或批准,这会导致:
- 长任务只能暂停在内存中,进程中断就得重头再来
- 只能让代理处理低风险任务,如研究和总结
- 让代理做更大更有用的事,但只能祈祷它不会出错
理解笔记
构建 Agent 过程中,使用上下文或者 code 控制流程,LLM 引导控制流的路径,而不是成为控制流本身。
- 控制流应由确定性代码主导,LLM 只负责决策建议,避免"黑盒"自动化带来的不可控风险。
- 为关键节点(如工具调用前)设计人工审核或中断机制,提升安全性。
- 将控制流逻辑与 LLM 推理解耦,便于测试和维护。
9. 将错误压缩到上下文窗口中
当 LLM 驱动的应用发生错误时,应向 LLM 提供简明、相关的故障信息。本原则建议将错误详情以结构化方式压缩进上下文窗口,让 LLM 更好地理解问题,并可能建议恢复策略或替代方案。这样可避免直接传递冗长原始日志,防止模型困惑。
这一点虽短,但很重要。代理的优势之一是"自我修复"——对于短任务,LLM 可能会调用失败的工具。优秀的 LLM 能很好地读取错误消息或堆栈跟踪,并判断后续工具调用需要如何调整。
class ErrorHandler:
def__init__(self):
self.error_counts = defaultdict(int)
self.max_retries = 3
def handle_error(self, error: Exception, context: str) -> str:
error_type = type(error).__name__
self.error_counts[error_type] += 1
if self.error_counts[error_type] > self.max_retries:
returnf"<critical_error>Too many {error_type} errors. Human intervention required.</critical_error>"
# 压缩错误信息
compressed_error = self.compress_error(error, context)
returnf"<e>{compressed_error}</e>"
def compress_error(self, error: Exception, context: str) -> str:
# 提取关键信息,过滤掉冗余的堆栈跟踪
if isinstance(error, ConnectionError):
returnf"Connection failed to {context}: {str(error)[:100]}"
elif isinstance(error, ValidationError):
return f"Validation error in {context}: {error.args[0] if error.args else 'Unknown validation issue'}"
else:
return f"{type(error).__name__} in {context}: {str(error)[:100]}"
上下文窗口中的错误表示:
<deploy_backend>
intent: "deploy_backend"
tag: "v1.2.3"
environment: "production"
</deploy_backend>
<e>部署服务连接失败: 无法连接到 https://deploy.company.com</e>
<request_more_information>
intent: "request_more_information_from_human"
question: "部署服务似乎不可用,我应该尝试其他方案还是等待?"
context: "由于连接问题,v1.2.3部署失败。"
</request_more_information>
在有限的上下文窗口中表示错误信息是一门艺术:既要保留足够的诊断信息让AI理解问题所在,又不能占用过多宝贵的上下文空间。您需要实现智能的错误分类、压缩和优先级排序机制,甚至可以在错误解决后将其从上下文中移除以节省空间。同时,建立错误计数器和阈值机制防止AI陷入重复错误的循环,当达到阈值时及时升级给人类处理。
** 错误恢复策略:**
def smart_error_recovery(thread: Thread, error: Exception):
if isinstance(error, RateLimitError):
# 对于速率限制,等待并重试
thread.events.append({
'type': 'rate_limit_hit',
'data': {'wait_time': error.retry_after}
})
return'wait_and_retry'
elif isinstance(error, AuthenticationError):
# 对于认证错误,请求人类干预
thread.events.append({
'type': 'auth_error',
'data': {'service': error.service}
})
return'request_human_help'
else:
# 对于其他错误,让LLM决定下一步
compressed_error = compress_error(error)
thread.events.append({
'type': 'error',
'data': compressed_error
})
return 'let_llm_decide'
理解笔记
构建 Agent 过程中,智能错误处理机制是构建可靠AI系统的关键组成部分。
- 错误信息应结构化、简明地反馈给 LLM,避免冗长日志导致模型困惑。
- 为常见错误类型设计标准化摘要模板,便于 LLM 理解和自我修复。
- 记录每次错误及其处理过程,持续优化错误处理策略。
10.小型、专注的 Agent
本原则主张将复杂 LLM 应用拆分为更小、更专业的代理,每个代理专注于不同任务或领域。模块化方法能减轻单个 LLM 的认知负担,通过缩小关注范围提升准确性,也让系统更易开发、测试和维护。小型代理也更易于微调和优化。
与其构建包揽一切的单体代理,不如构建小型、专注、专业的代理。代理只是更大、更确定性系统的一个构件。
关键洞察在于 LLM 的局限性:任务越大越复杂,步骤越多,上下文窗口越长,LLM 越容易迷失或分心。让代理专注于特定领域,并将流程控制在 3-10 步、最多 20 步内,有助于保持上下文可控,提升 LLM 性能。
随着上下文扩大,LLM 更容易迷失或分心
小型、专注代理的优势:
- 可控上下文:窗口越小,性能越好
- 职责明确:每个代理有清晰范围和目标
- 更高可靠性:减少复杂流程中迷失的风险
- 易于测试:便于验证特定功能
- 调试友好:更易定位和修复问题
理解笔记
构建 Agent 过程中,设计专注于特定领域的小型 Agent。
- 将复杂任务拆分为多个小型、专注的 Agent,每个 Agent 只负责单一领域或流程。
- 为每个 Agent 明确职责边界,便于独立开发、测试和优化。
- 通过组合多个 Agent 实现复杂业务,提升系统灵活性和可维护性。
11. 随时随地触发,随时随地与用户见面
可靠的 LLM 应用应能从多种入口和界面访问和触发。无论网页聊天、API、移动端还是内部仪表盘,代理都应能无缝接收输入并响应。这种灵活性确保代理能集成进现有流程,并在用户偏好的平台上互动,提升可用性和采纳率。
允许用户通过 Slack、邮件、短信等任意渠道触发客服,客服也可通过同样渠道响应。
这样做的好处:
- 在用户所在的地方与其见面:有助于打造"像真人或数字同事"的 AI 应用
- 外环代理:支持代理被事件、定时任务等非人类触发,关键节点可寻求人类协助
- 高风险工具:快速整合多方人员,让代理能安全执行高风险操作(如发外部邮件、更新生产数据),提升可审计性和信心
理解笔记
构建 Agent 过程中,使用多渠道接入用户,似乎对国内的封闭系统存在一定的冲击。
- 支持多种入口和渠道(如 Web、API、IM、定时任务等)触发和交互,提升系统可用性。
- 为每个渠道设计统一的接入和认证机制,确保安全和一致体验。
- 关注国内外不同生态的集成方式,提升系统兼容性。
12. 让你的 Agent 成为无状态 Reducer
本原则鼓励将 LLM 代理设计为无状态处理器,每次只依赖输入和外部状态生成输出,无需在交互间维护内部长期记忆。所有持久状态应交由外部存储(如数据库、消息队列)管理。这种模式提升了可扩展性、弹性和水平扩展能力,任何代理实例都能处理任意请求,单实例故障也不会丢失数据。
理解笔记
构建 Agent 过程中,采用无状态的纯函数的转换器方式,更能满足扩展、测试等需求。
- Agent 应设计为无状态的"Reducer",每次只依赖输入和外部状态,输出结果。
- 将所有持久化状态交由外部存储系统管理,提升系统弹性和可扩展性。
- 无状态设计有助于水平扩展和故障恢复,是生产级 Agent 的基础。
这些原则如何落地
电商平台的智能客服
假设您在构建一个电商平台的智能客服系统。按照12-Factor原则,您不会直接使用某个Agent框架,而是:首先设计清晰的工具调用接口(查询订单、处理退款等),然后精心设计提示词来处理常见的客服场景,接下来实现状态管理来跟踪每个对话的进展,最后设计简单的API来让人工客服能够随时介入。这样构建的系统不仅更可靠,也更容易维护和扩展。
内容创作助手的设计思路
如果您在开发内容创作助手,可以这样应用这些原则:将不同的创作任务(标题生成、大纲规划、段落写作)设计为独立的小Agent,每个Agent专注于自己的领域。用户可以从任何平台(Web、移动App、浏览器插件)触发这些Agent,系统会根据用户的创作进度智能地组织上下文信息。这种设计既提高了创作效率,又保持了用户的创作控制权。
参考资料
[1] 12-Factor Agents: https://github.com/humanlayer/12-factor-agents
本文转载自AI 博物院 作者:longyunfeigu
