CrabNote螃蟹笔记

12-Factor Agents:构建生产级AI智能体的工程原则

12-Factor Agents:构建生产级AI智能体的工程原则

12 Factor Agents:构建生产级AI智能体的工程原则 12 Factor Agents:构建生产级AI智能体的工程原则 Modified September 15, 2025 将自然语言转换为结构化工具调用是Agent构建中最常见的模式。 这是一个强大的模式,允许你构建能够推理任务并执行它们的智能体。 核心思想: 将像"为Jeff创建750美元的付款链接用于赞助2月AI聚会"这样的自然语言短语转换为描述Stripe API调用的结构化对象: Code block Plain Text { "function": { "name": "create payment link", "parameters": { "amount": 750, "customer": "cust 128934ddasf9", "product": "prod 8675309", "price": "prc 09874329fds", "quantity": 1, "memo": "Hey Jeff see below for the payment link for the february ai tinkerers meetup" } }} 然后确定性代码可以接收这个载荷并处理它: LLM接收自然语言并返回结构化对象 nextStep = await llm.determineNextStep( """ create a payment link for $750 to Jeff for sponsoring the february AI tinkerers meetup """ ) 基于结构化输出的函数处理 Code block Plain Text if nextStep.function == 'create payment link': stripe.paymentlinks.create(nextStep.parameters) returnelif nextStep.function == 'something else': ... more cases passelse: the model didn't call a tool we know about do something else pass 2. 拥有你的提示词 (Own Your Prompts) 不要将提示词工程外包给框架。 为什么重要: 提示词是你与LLM沟通的唯一方式,就像API接口一样重要。使用框架的黑盒方式会失去对Agent行为的精确控制。 推荐的方式是将提示词作为代码来管理: Code block Plain Text function DetermineNextStep(thread: string) DoneForNow | ListGitTags | DeployBackend | DeployFrontend | RequestMoreInformation { prompt " {{ .role("system") }} You are a helpful assistant that manages deployments for frontend and backend systems. You work diligently to ensure safe and successful deployments by following best practices and proper deployment procedures. Before deploying any system, you should check: The deployment environment (staging vs production) The correct tag/version to deploy The current system status You can use tools like deploy backend, deploy frontend, and check deployment status to manage deployments. For sensitive deployments, use request approval to get human verification. Always think about what to do first, like: Check current deployment status Verify the deployment tag exists Request approval if needed Deploy to staging before production Monitor deployment progress {{ .role("user") }} {{ thread }} What should the next step be? " } 3. 拥有你的上下文 (Own Your Context Window) 你不一定需要使用标准的基于消息的格式来向LLM传达上下文。 核心理念: 在任何给定时刻,你对Agent中LLM的输入都是"到目前为止发生了什么,下一步是什么"。 一切都是上下文工程。LLM是将输入转换为输出的无状态函数。要获得最佳输出,你需要给它们最佳输入。 自定义格式示例: Code block Plain Text <slack message From: @alex Channel: deployments Text: Can you deploy the backend?</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's the next step?实现代码: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 | stringdef event to prompt(event: Event) str: data = event.data if isinstance(event.data, str) \ else stringifyToYaml(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) 4. 工具就是结构化输出 (Tools Are Structured Outputs) 工具不需要复杂。在其核心,它们只是来自LLM的结构化输出,触发确定性代码。 核心模式: 1. LLM输出结构化JSON 2. 确定性代码执行适当的操作(如调用外部API) 3. 结果被捕获并反馈到上下文中 这在LLM的决策制定和应用程序的操作之间创建了清晰的分离。LLM决定做什么,但你的代码控制如何做。 工具定义示例: Code block Plain Text class Issue: title: str description: str team id: str assignee id: strclass CreateIssue: intent: "create issue" issue: Issueclass SearchIssues: intent: "search issues" query: str what youre looking for: str 统一执行状态和业务状态 (Unify Execution State) 即使在AI世界之外,许多基础设施系统也试图将"执行状态"与"业务状态"分开。 对于AI应用,这可能涉及复杂的抽象来跟踪当前步骤、下一步、等待状态、重试计数等。这种分离创造了可能值得的复杂性,但对你的用例可能是过度的。 核心思想: 如果可能,简化——尽可能统一这些。 • 执行状态:当前步骤、下一步、等待状态、重试计数等 • 业务状态:到目前为止在智能体工作流中发生的事情(例如OpenAI消息列表、工具调用和结果列表等) 实际上,你可以设计你的应用程序,使你可以从上下文窗口推断所有执行状态。 在许多情况下,执行状态(当前步骤、等待状态等)只是关于到目前为止发生了什么的元数据。 6. 启动/暂停/恢复 (Launch/Pause/Resume) 通过简单的API支持Agent的启动、暂停和恢复功能,这对需要人工干预或长时间运行的任务至关重要。 实际需求: Agent可能需要等待人工批准或长时间运行的任务完成,这时需要能够暂停执行并稍后恢复。 7. 通过工具调用联系人类 (Contact Humans with Tool Calls) 默认情况下,LLM API依赖于一个基本的高风险token选择:我们是返回纯文本内容,还是返回结构化数据? 核心问题: 你在第一个token的选择上投入了很大的权重,在"东京的天气"情况下是"the",但在fetch weather情况下,它是表示JSON对象开始的特殊token |JSON 。 解决方案: 你可能通过让LLM总是输出json,然后用一些自然语言token如request human input或done for now来声明其意图(而不是像check weather in city这样的"适当"工具)来获得更好的结果。 人类交互工具示例: Code block Plain Text Tool definition for human interactionclass RequestHumanInput: intent: "request human input" question: str context: str options: Options Example usage in the agent loopif 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 Break loop and wait for response to come back with thread IDelse: ... other cases 8. 拥有你的控制流 (Own Your Control Flow) 构建适合你特定用例的自定义控制结构。 具体来说,某些类型的工具调用可能是跳出循环并等待来自人类或其他长时间运行任务(如训练管道)的响应的原因。 不同工具调用需要不同处理方式: def handle next step(thread: Thread): while True: next step = await determine next step(thread to prompt(thread)) Code block Plain Text 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) async step break the loop, we'll get a webhook later 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, }) sync step pass the new context to the LLM to determine the NEXT next step continue 9. 将错误压缩到上下文窗口 (Compact Errors) 这个比较简短但值得一提。智能体的好处之一是"自我修复"——对于短任务,LLM可能调用失败的工具。好的LLM有相当好的机会读取错误消息或堆栈跟踪,并找出在后续工具调用中要更改的内容。 当工具调用失败时,将错误信息添加到上下文中,LLM能理解错误并调整操作: Code block Plain Text 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) our switch statement except Exception as e: if we get an error, we can add it to the context window and try again thread["events"].append({ "type": 'error', "data": format error(e), }) Wiki pages you might want to explore: • The Agentic Loop (humanlayer/12 factor agents)([1]) 这个比较简短但值得一提。智能体的好处之一是"自我修复"——对于短任务,LLM可能调用失败的工具。好的LLM有相当好的机会读取错误消息或堆栈跟踪,并找出在后续工具调用中要更改的内容。 当工具调用失败时,将错误信息添加到上下文中,LLM能理解错误并调整操作: 你可能还想实现一个错误计数器来限制特定工具调用的重试次数,比如限制为3次,或者其他适合你用例的逻辑。 10. 小而专注的智能体 (Small, Focused Agents) 构建小而专注的智能体,而不是试图做所有事情的单体智能体。智能体只是更大的、主要是确定性系统中的一个构建块。 关键洞察: 这关乎LLM的限制:任务越大越复杂,需要的步骤就越多,意味着更长的上下文窗口。随着上下文增长,LLM更容易迷失或失去焦点。通过保持智能体专注于特定领域,控制在3 10步,最多20步,我们保持上下文窗口可管理,LLM性能高。 即使LLM变得更聪明怎么办? 随着智能体和LLM的改进,它们可能自然扩展到能够处理更长的上下文窗口。这种小而专注的方法确保你今天就能获得结果,同时为随着LLM上下文窗口变得更可靠而慢慢扩展智能体范围做好准备。 11. 从任何地方触发 (Trigger from Anywhere) 智能体应该能够从任何地方触发——用户界面、API、webhook、定时任务等,在用户所在的地方与他们见面。 核心理念: 用户希望在他们习惯的地方与智能体交互,而不是被迫使用特定的界面。这意味着智能体应该支持多种触发方式: • Web界面 • API调用 • Slack/Discord机器人 • 邮件触发 • 定时任务 • Webhook响应 12. 让智能体成为无状态的归约器 (Stateless Reducer 将你的智能体设计为无状态的归约器,所有状态都包含在线程/上下文中,使智能体可以在任何时候暂停和恢复。 核心概念: 智能体应该像一个纯函数一样工作:给定相同的输入(上下文),总是产生相同的输出(下一步行动)。所有必要的状态信息都应该包含在传递给智能体的上下文中。 这种设计的好处: • 可恢复性:可以在任何时候暂停和恢复 • 可扩展性:可以在不同的服务器上运行 • 可调试性:所有状态都是显式的 • 可测试性:容易编写单元测试 这个例子展示了智能体如何与确定性代码协作:大部分流程是自动化的,智能体只在需要理解人类反馈和做出决策的关键节点介入。 总之,12 Factor Agents的核心理念是:智能体不是万能的自主程序,而是在传统软件中加入智能决策能力的工具。通过这些工程原则,可以构建既智能又可靠的生产级AI应用。 Reference[1] https://github.com/humanlayer/12 factor agents 将自然语言转换为结构化工具调用是Agent构建中最常见的模式。 这是一个强大的模式,允许你构建能够推理任务并执行它们的智能体。 核心思想: 将像"为Jeff创建750美元的付款链接用于赞助2月AI聚会"这样的自然语言短语转换为描述Stripe API调用的结构化对象: 然后确定性代码可以接收这个载荷并处理它: LLM接收自然语言并返回结构化对象 nextStep = await llm.determineNextStep( """ create a payment link for $750 to Jeff for sponsoring the february AI tinkerers meetup """ ) 基于结构化输出的函数处理 2. 拥有你的提示词 (Own Your Prompts) 不要将提示词工程外包给框架。 为什么重要: 提示词是你与LLM沟通的唯一方式,就像API接口一样重要。使用框架的黑盒方式会失去对Agent行为的精确控制。 推荐的方式是将提示词作为代码来管理: 3. 拥有你的上下文 (Own Your Context Window) 你不一定需要使用标准的基于消息的格式来向LLM传达上下文。 核心理念: 在任何给定时刻,你对Agent中LLM的输入都是"到目前为止发生了什么,下一步是什么"。 一切都是上下文工程。LLM是将输入转换为输出的无状态函数。要获得最佳输出,你需要给它们最佳输入。 自定义格式示例: 4. 工具就是结构化输出 (Tools Are Structured Outputs) 工具不需要复杂。在其核心,它们只是来自LLM的结构化输出,触发确定性代码。 核心模式: 1. LLM输出结构化JSON 2. 确定性代码执行适当的操作(如调用外部API) 3. 结果被捕获并反馈到上下文中 这在LLM的决策制定和应用程序的操作之间创建了清晰的分离。LLM决定做什么,但你的代码控制如何做。 工具定义示例: 统一执行状态和业务状态 (Unify Execution State) 即使在AI世界之外,许多基础设施系统也试图将"执行状态"与"业务状态"分开。 对于AI应用,这可能涉及复杂的抽象来跟踪当前步骤、下一步、等待状态、重试计数等。这种分离创造了可能值得的复杂性,但对你的用例可能是过度的。 核心思想: 如果可能,简化——尽可能统一这些。 • 执行状态:当前步骤、下一步、等待状态、重试计数等 • 业务状态:到目前为止在智能体工作流中发生的事情(例如OpenAI消息列表、工具调用和结果列表等) 实际上,你可以设计你的应用程序,使你可以从上下文窗口推断所有执行状态。 在许多情况下,执行状态(当前步骤、等待状态等)只是关于到目前为止发生了什么的元数据。 6. 启动/暂停/恢复 (Launch/Pause/Resume) 通过简单的API支持Agent的启动、暂停和恢复功能,这对需要人工干预或长时间运行的任务至关重要。 实际需求: Agent可能需要等待人工批准或长时间运行的任务完成,这时需要能够暂停执行并稍后恢复。 7. 通过工具调用联系人类 (Contact Humans with Tool Calls) 默认情况下,LLM API依赖于一个基本的高风险token选择:我们是返回纯文本内容,还是返回结构化数据? 核心问题: 你在第一个token的选择上投入了很大的权重,在"东京的天气"情况下是"the",但在fetch weather情况下,它是表示JSON对象开始的特殊token |JSON 。 解决方案: 你可能通过让LLM总是输出json,然后用一些自然语言token如request human input或done for now来声明其意图(而不是像check weather in city这样的"适当"工具)来获得更好的结果。 人类交互工具示例: 8. 拥有你的控制流 (Own Your Control Flow) 构建适合你特定用例的自定义控制结构。 具体来说,某些类型的工具调用可能是跳出循环并等待来自人类或其他长时间运行任务(如训练管道)的响应的原因。 不同工具调用需要不同处理方式: def handle next step(thread: Thread): while True: next step = await determine next step(thread to prompt(thread)) 9. 将错误压缩到上下文窗口 (Compact Errors) 这个比较简短但值得一提。智能体的好处之一是"自我修复"——对于短任务,LLM可能调用失败的工具。好的LLM有相当好的机会读取错误消息或堆栈跟踪,并找出在后续工具调用中要更改的内容。 当工具调用失败时,将错误信息添加到上下文中,LLM能理解错误并调整操作: Wiki pages you might want to explore: • The Agentic Loop (humanlayer/12 factor agents)([1]) 这个比较简短但值得一提。智能体的好处之一是"自我修复"——对于短任务,LLM可能调用失败的工具。好的LLM有相当好的机会读取错误消息或堆栈跟踪,并找出在后续工具调用中要更改的内容。 当工具调用失败时,将错误信息添加到上下文中,LLM能理解错误并调整操作: 你可能还想实现一个错误计数器来限制特定工具调用的重试次数,比如限制为3次,或者其他适合你用例的逻辑。 10. 小而专注的智能体 (Small, Focused Agents) 构建小而专注的智能体,而不是试图做所有事情的单体智能体。智能体只是更大的、主要是确定性系统中的一个构建块。 关键洞察: 这关乎LLM的限制:任务越大越复杂,需要的步骤就越多,意味着更长的上下文窗口。随着上下文增长,LLM更容易迷失或失去焦点。通过保持智能体专注于特定领域,控制在3 10步,最多20步,我们保持上下文窗口可管理,LLM性能高。 即使LLM变得更聪明怎么办? 随着智能体和LLM的改进,它们可能自然扩展到能够处理更长的上下文窗口。这种小而专注的方法确保你今天就能获得结果,同时为随着LLM上下文窗口变得更可靠而慢慢扩展智能体范围做好准备。 11. 从任何地方触发 (Trigger from Anywhere) 智能体应该能够从任何地方触发——用户界面、API、webhook、定时任务等,在用户所在的地方与他们见面。 核心理念: 用户希望在他们习惯的地方与智能体交互,而不是被迫使用特定的界面。这意味着智能体应该支持多种触发方式: • Web界面 • API调用 • Slack/Discord机器人 • 邮件触发 • 定时任务 • Webhook响应 12. 让智能体成为无状态的归约器 (Stateless Reducer 将你的智能体设计为无状态的归约器,所有状态都包含在线程/上下文中,使智能体可以在任何时候暂停和恢复。 核心概念: 智能体应该像一个纯函数一样工作:给定相同的输入(上下文),总是产生相同的输出(下一步行动)。所有必要的状态信息都应该包含在传递给智能体的上下文中。 这种设计的好处: • 可恢复性:可以在任何时候暂停和恢复 • 可扩展性:可以在不同的服务器上运行 • 可调试性:所有状态都是显式的 • 可测试性:容易编写单元测试 这个例子展示了智能体如何与确定性代码协作:大部分流程是自动化的,智能体只在需要理解人类反馈和做出决策的关键节点介入。 总之,12 Factor Agents的核心理念是:智能体不是万能的自主程序,而是在传统软件中加入智能决策能力的工具。通过这些工程原则,可以构建既智能又可靠的生产级AI应用。 Reference[1] https://github.com/humanlayer/12 factor agents 拓展阅读:https://mp.weixin.qq.com/s/X OmF2am9w3JUQLuTODYQw 你有没有想过,为什么AI能理解你说的话,但很多AI应用在实际使用中却总是"翻车"?问题出在哪里? 想象一下:你让AI帮你订餐,它理解了你的需求,但却不知道怎么调用外卖API; 或者它能调用API,但处理错误时就"死机"了。 这就是为什么我们读一读12 Factor Agents这个项目—— https://github.com/humanlayer/12 factor agents 一套让AI真正"靠谱"的工程原则。 帮助开发者构建真正可投入生产的AI智能体。 Agent核心循环 Agent通过循环决策来工作: ——下面是根据原文翻译改写的12个原则详解 1. 自然语言到工具调用 (Natural Language to Tool Calls)