Karpathy 深入讲解如何用 AI 生成和处理函数级代码。对编程工作流改进有直接指导价值。
观看:AI Functions 深度解析
Andrej Karpathy 提出了一套描述软件编写方式的版本编号方案。Software 1.0 是人类编写的代码。Software 2.0 是通过优化学习得到的神经网络权重。Software 3.0 是使用自然语言提示 LLM——这个叫法听起来比“凭感觉编程”更好听;顺带一提,“凭感觉编程”这个术语同样是 Karpathy 创造的。
当然,Software 3.0 确实存在,而且每天都有数百万人在使用。Kiro、Cursor、Claude Code 和 ChatGPT 等工具允许你描述自己的需求,然后获得代码。Karpathy 强调了部分自治工具中的“生成—验证循环”:模型生成变更,人类进行验证,整个工作过程如此迭代。
但这里发生的事情,比由谁审查什么更加根本。看看 LLM 在 Software 3.0 中实际生成的内容:文本。字符串形式的代码。JSON 载荷。Markdown 文档。模型完成生成,你收到文本,然后其余一切都由你来做——将其集成到代码库、编写测试、运行 CI、部署。如果你严格执行验证流程,就会编写测试用例,但这些测试是在部署前运行的。一旦代码发布,测试便不会再次执行。LLM 在把输出交给你的那一刻就结束了参与。正在运行的软件与帮助编写它的模型之间没有任何关系。
现在考虑一种不同的安排。LLM 生成的代码真正运行在你的应用程序内部——在调用时运行,并且函数每次被调用时都会运行。它返回原生 Python 对象——DataFrame、Pydantic 模型、数据库连接——而不是需要你解析的 JSON 字符串。验证也不再是部署前必须通过的一道关卡,而是在每次调用时执行的后置条件;验证失败会反馈给模型,以便自动重试。这同时改变了三件事:AI 在软件中的位置(运行时,而不只是开发时)、它生成的内容(可以调用方法的活对象,而不是序列化文本),以及你信任它的方式(持续的自动验证,而不是一次性人工审查)。
这正是 AI Functions 核心的实验。AI Functions 是 Strands Labs 基于 Strands Agents SDK 构建的一个新项目。你编写一个 Python 函数,用自然语言规范代替实现代码。你为它附加后置条件——使用普通 Python 断言定义正确输出应当是什么样子。当函数被调用时,LLM 会生成代码,在你的 Python 进程中执行代码,将结果作为原生对象返回,然后由后置条件进行验证。如果验证失败,系统会将错误作为反馈并重试。人类完全不需要检查生成的代码。每一次,都由后置条件完成检查。
如果说 Software 3.0 是“人类给出提示、LLM 生成、人类验证”,那么我认为 AI Functions 就是 Software 3.1:人类制定规范、LLM 生成并执行、机器验证——而且是在运行时。范式仍然相同——将自然语言作为编程接口。但执行模型有所不同。LLM 生成的不是供人类集成的文本,而是实际运行的代码;它返回应用程序可以直接使用的对象,并在每次调用时通过后置条件进行验证。Software 3.1 是一个“小版本更新”,而不是一次大版本升级。升级之处在于生成之后发生的事情。
本文将深入探讨 AI Functions 是什么、它如何工作,以及自动验证能够带来哪些可能性。
AI Functions 构建在 Strands Agents SDK 之上,后者是一个用于构建 AI 智能体的开源框架。AI Functions 引入了一个核心抽象:@ai_function 装饰器。你编写一个 Python 函数,使用自然语言规范代替实现函数体。当函数被调用时,LLM 会生成实现、执行它并返回结果。你还可以选择附加用于验证输出的后置条件——这也是最重要的部分——如果验证失败,就会触发自动重试。
最简单的示例如下:
1
2
3
4
5
6
7
8
9
10
from ai_functions import ai_function
@ai_function
def translate_text(text: str, lang: str) -> str:
"""
Translate the text below to the following language: {lang}.
{text}
"""
result = translate_text("The quarterly results exceeded expectations.", lang="French")
你可以像调用任何 Python 函数一样调用 translate_text。装饰器会拦截这次调用,根据文档字符串构造提示(将参数替换进去),把它发送给 LLM,然后以带类型的 Python 字符串形式返回结果。从调用者的角度看,这只是一个接收字符串并返回字符串的函数。由 LLM 执行这一事实只是一个实现细节。
仅凭这一点,它多少仍然属于 Software 3.0——输入提示,输出结果。用起来很舒服,但这并不是 AI Functions 真正有趣的地方。当你加入结构化能力、验证、代码执行、多智能体组合和异步工作流时,它才真正变得有趣。Software 3.1 正是从这里开始的。
AI Functions 可以返回任意带类型的对象,而不仅仅是字符串。当你指定一个 Pydantic 模型作为返回类型时,框架会自动强制输出符合该 schema:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from ai_functions import ai_function
from pydantic import BaseModel
class MeetingSummary(BaseModel):
attendees: list[str]
key_decisions: list[str]
action_items: list[str]
@ai_function
def summarize_meeting(transcript: str) -> MeetingSummary:
"""
Summarize the following meeting transcript in less than 50 words.
<transcript>
{transcript}
</transcript>
"""
调用 summarize_meeting(transcript) 后,你会得到一个 MeetingSummary 对象,它拥有带类型的字段、IDE 自动补全以及 Pydantic 内置的验证能力。LLM 的输出会被解析为 Pydantic 模型;如果结构不匹配,框架会负责重试。从调用者的角度看,该函数返回的是一个带类型的 Python 对象。
这是 Instructor 等框架已经确立的一种模式。AI Functions 的贡献并不在结构化输出本身,而在于结构化输出如何与系统中的其他一切组合起来。
后置条件是让 AI Functions 不只是一个提示框架的核心所在。后置条件是一个用于验证 AI Function 输出的 Python 函数。如果验证失败,错误消息会反馈给 LLM,由它进行重试。多个后置条件会并行运行,因此 LLM 可以一次性收到所有失败信号,并在一次重试中处理这些问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from ai_functions import ai_function, PostConditionResult
from pydantic import BaseModel
class MeetingSummary(BaseModel):
attendees: list[str]
key_decisions: list[str]
action_items: list[str]
def check_length(response: MeetingSummary):
total = sum(len(d.split()) for d in response.key_decisions)
assert total <= 50, f"Key decisions should total under 50 words, got {total}"
@ai_function
def check_quality(response: MeetingSummary) -> PostConditionResult:
"""
Check if the meeting summary below satisfies the following criteria:
- Key decisions must be specific and actionable, not vague
- Action items must each name a responsible person
<decisions>{response.key_decisions}</decisions>
<actions>{response.action_items}</actions>
"""
@ai_function(post_conditions=[check_length, check_quality])
def summarize_meeting(transcript: str) -> MeetingSummary:
"""
Summarize the following meeting transcript in less than 50 words.
<transcript>
{transcript}
</transcript>
"""
这里有两点值得注意。第一,check_length 是一个普通的 Python 函数,失败时会抛出 AssertionError。这是一种确定性的、可检查的验证——不涉及 LLM,也不存在歧义。第二,check_quality 本身也是一个 AI Function,它返回 PostConditionResult——一个包含 passed(bool)和 message(str)字段的 Pydantic 模型。它使用 LLM 来评估摘要是否满足那些很难用断言表达的质量标准——具体性、可执行性和责任归属。一个 AI Function 验证另一个 AI Function。框架对二者一视同仁:只要其中任意一个失败,错误就会作为反馈返回给负责生成的 LLM。
这形成了一个自我纠正的循环。负责生成的 LLM 不需要第一次就给出正确结果。它需要具备的能力,是在获得有关具体错误的反馈后进行改进。在实践中,这意味着开发者的工作会从精心设计完美提示,转变为编写良好的后置条件——这是一项从根本上不同的技能。
当然,我们需要理解这里究竟发生了什么,也需要意识到,这可能会让项目中出现“隐藏”的代码重试循环!在过度依赖它之前,我们必须确保拥有可靠的监控和可观测性。
大多数 LLM 框架强制输出通过 JSON 序列化。AI Functions 可以返回非可序列化的 Python 对象 —— DataFrames、SymPy 表达式、数据库连接,任何东西 —— 因为生成的代码在与应用程序相同的 Python 解释器中运行。
这是使 AI Functions 在质量上与其他框架不同的特性。考虑一个格式无关的数据加载器,它可以处理购买记录,而不管它们如何存储:
from ai_functions import ai_function
from pandas import DataFrame, api
def check_invoice_dataframe(df: DataFrame):
"""Post-condition: validate DataFrame structure."""
assert {'product_name', 'quantity', 'price', 'purchase_date'}.issubset(df.columns)
assert api.types.is_integer_dtype(df['quantity']), "quantity must be an integer"
assert api.types.is_float_dtype(df['price']), "price must be a float"
assert api.types.is_datetime64_any_dtype(df['purchase_date'])
assert not df.duplicated(subset=['product_name', 'purchase_date']).any()
@ai_function(
code_execution_mode="local",
code_executor_additional_imports=["pandas.*", "sqlite3", "json"],
post_conditions=[check_invoice_dataframe],
)
def import_invoice(path: str) -> DataFrame:
"""
The file `{path}` contains purchase logs. Extract them in a DataFrame with columns:
- product_name (str)
- quantity (int)
- price (float)
- purchase_date (datetime)
"""
调用 import_invoice('data/orders.json'),你会得到一个实际的 Pandas DataFrame —— 不是它的 JSON 表示,不是序列化的字符串,而是一个真正的 DataFrame 对象,你可以立即在其上调用 .describe()、.groupby() 或 .plot()。改为传递给它一个 SQLite 文件,相同的函数会检查数据库 schema,编写适当的 SQL 查询,并返回相同的验证过的 DataFrame 结构。
开发者不需要编写任何格式特定的解析逻辑。自然语言规范说明了输出应该包含什么。后置条件验证结构不变量。LLM 在调用时动态地弄清楚如何从不透明文件到验证的 DataFrame。
这之所以有效,是因为框架为 LLM 提供了一个与调用代码共享相同运行时的 Python 执行器工具。LLM 生成 Python 代码,在你的进程内执行它,并直接返回结果对象。没有序列化往返。code_execution_mode="local" 参数是显式的选择加入 —— 框架默认不运行任意生成的代码,你声明允许哪些导入。
代码执行模型值得更仔细的关注,因为它揭示了 AI Functions 对信任的深思熟虑的方法。
当启用 code_execution_mode="local" 时,LLM 可以在你的解释器中生成和执行 Python 代码。这很强大 —— 它使返回 DataFrames、运行计算和与本地环境交互成为可能。但这也是一个安全面。框架通过多种机制来缓解这个问题:
显式选择加入。 代码执行默认是关闭的。你必须为每个函数启用它。
导入限制。 code_executor_additional_imports 显式声明生成的代码可以使用哪些包。任何未列出的内容都不可用。
后置条件验证。 输出被验证,无论它如何生成。即使生成的代码走了意外的路径,后置条件也会捕获无效的结果。
但老实说,这是一种权衡。你在进程中执行 LLM 生成的代码。框架使用基于 AST 的生成代码验证,具有受控的导入和超时,试图防止恶意导入并阻止危险操作。但这不提供真正的沙箱化,也不能防止资源耗尽(无限循环、过度内存分配)。对于实验,在适当的约束下,这是一个合理的选择。对于生产工作负载,项目建议在容器或其他隔离环境中运行 AI Functions,以提供进程级隔离。
AI Functions 的结果通过常规 Python 自然组合。由于 AI Functions 返回类型对象,你以与链接任何函数相同的方式链接它们 —— 通过将输出作为输入传递:
from ai_functions import ai_function
from pandas import DataFrame
@ai_function(code_execution_mode="local", code_executor_additional_imports=["pandas.*"])
async def analyze_sales_data(path: str) -> DataFrame:
"""
Load the sales data from `{path}` and compute a summary DataFrame
with total revenue, average order value, and top 5 products by volume.
"""
@ai_function
def write_executive_summary(company: str, financials: DataFrame) -> str:
"""
Write a concise executive summary for {company} highlighting key trends
and recommendations based on the provided financial data.
"""
financials = await analyze_sales_data("data/q4_sales.csv")
summary = write_executive_summary("Acme Corp", financials)
print("Top Products:", financials.head())
print("Summary:", summary)
这只是普通的函数组合。第一个函数返回 DataFrame;第二个函数将 DataFrame 作为输入。不需要特殊的状态传递机制。
对于更复杂的工作流,AI Functions 可以被其他智能体用作工具,启用协调器委托给专门化子智能体的编排模式:
from ai_functions import ai_function
from ai_functions.types import PostConditionResult
from pydantic import BaseModel, Field
from typing import Literal
@ai_function(
description="Search the web for a topic and return a cited summary",
tools=[websearch_tool],
post_conditions=[check_length, check_citations],
)
def search_agent(query: str, max_words: int = 500) -> str:
"""
Perform a web search on the following topic and return a summary.
Every claim must be supported by citations to sources.
<query>{query}</query>
"""
@ai_function(
description="Suggest the plan and organization of a report",
tools=[websearch_tool],
)
def report_planner(topic: str) -> ReportPlan:
"""Generate a plan to write a report on: {topic}"""
@ai_function(tools=[report_planner, search_agent, report.add_section])
def report_orchestrator(topic: str) -> Literal["done"]:
"""
Write a report on the following topic: {topic}
"""
编排器将 report_planner、search_agent 和 report.add_section 视为可调用的工具。每个子智能体以自己的后置条件运行,因此编排器接收验证的结果。搜索智能体的引用在其结果到达编排器之前被验证。这创建了一个验证的智能体层级 —— 后置条件在多智能体系统中组合。
AI Functions 可以定义为 async,这使得独立任务的并行执行成为可能:
from ai_functions import ai_function
import asyncio
import pandas as pd
@ai_function(tools=[websearch_tool])
async def research_market(company: str) -> str:
"""Research and summarize the competitive landscape and recent news for: {company}"""
@ai_function(code_execution_mode="local", code_executor_additional_imports=["pandas.*", "yfinance.*"])
async def load_financial_data(stock: str) -> pd.DataFrame:
"""
Use the `yfinance` Python package to retrieve the historical prices of {stock}
in the last 30 days. Return a DataFrame with columns [date, price].
"""
@ai_function(code_execution_mode="local", code_executor_additional_imports=["pandas.*", "plotly.*"])
def write_investment_memo(company: str, research: str, financials: pd.DataFrame) -> str:
"""
Write an investment memo for {company}. Use the market research and financial data:
{research}
"""
async def due_diligence_workflow(company: str):
research, financials = await asyncio.gather(
research_market(company),
load_financial_data(company)
)
write_investment_memo(company, research, financials)
这两个任务并发运行。由于它们是独立的 —— 一个搜索网络,一个加载和转换本地数据 —— 并行性给你大致一半的墙钟时间的相同结果,没有额外成本。结果然后输入到使用两者的同步备忘录编写器。
注意 tools=[websearch_tool] 参数。AI Functions 可以使用任何 Strands 工具。框架为 Python 代码执行提供了内置工具,你可以为每个函数传递额外的工具(网络搜索、API 客户端、文件 I/O)。LLM 在执行期间决定何时以及如何使用它们。
工作流的不同部分可能需要不同的模型。快速验证检查不需要与复杂分析相同的模型。AI Functions 使用 AIFunctionConfig 对象在函数之间共享配置:
from ai_functions import ai_function, AIFunctionConfig
from pandas import DataFrame
class Configs:
BIG_MODEL = AIFunctionConfig(model="us.anthropic.claude-sonnet-4-5-20250929-v1:0")
FAST_MODEL = AIFunctionConfig(model="us.anthropic.claude-haiku-4-5-20251001-v1:0")
DATA_ANALYSIS = AIFunctionConfig(
model="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
code_execution_mode="local",
code_executor_additional_imports=["pandas.*", "numpy.*"],
)
@ai_function(config=Configs.DATA_ANALYSIS)
def normalize_dataset(path: str) -> DataFrame:
"""Load, clean, and normalize the dataset at `{path}` into a standard schema."""
@ai_function(config=Configs.FAST_MODEL)
def validate_email(text: str) -> bool:
"""Check if the following string is a valid email address: {text}"""
Configs 是纯 Python 对象,因此将整个管道从一个模型族切换到另一个只需改一行。开发过程中,你可能会将所有内容路由到一个功能强大但昂贵的模型。为了成本优化,你可以交换配置的模型引用,看看会出现什么问题。@ai_function 上的关键字参数会覆盖各个函数的配置值,因此你可以专门化而不需要复制整个配置。
后置条件系统更微妙的功能之一是验证结果的属性,这些属性很难表示为结构检查。AI 驱动的后置条件允许你评估语义质量——事实性、引用质量、逻辑一致性——使用一个 LLM 来验证另一个:
from ai_functions import ai_function, PostConditionResult
@ai_function
def check_citations(summary: str) -> PostConditionResult:
"""
Validate if all the claims made in the following summary are supported
by an inline citation to a credible source.
<summary>
{summary}
</summary>
"""
def check_length(summary: str, max_words: int):
assert len(summary.split()) <= max_words
@ai_function(
tools=[websearch_tool],
post_conditions=[check_length, check_citations],
)
def market_researcher(query: str, max_words: int = 500) -> str:
"""
Research and provide a well-sourced answer to: {query}
Every claim must be supported by citations to credible sources.
"""
研究智能体生成一个摘要。check_length 确定性地验证字数。check_citations 使用 LLM 评估每个声明是否实际上由引用的来源支持。如果智能体在没有进行真正研究的情况下幻觉出答案,引用检查会捕获它并触发重试,反馈关于具体哪些声明缺少来源。
这是与检查输出结构不同的验证方式。它使用 AI 来验证 AI——检查难以表示为断言的语义属性。它解决了 LLM 系统中最棘手的问题之一:你如何知道模型没有只是编造东西?后置条件不能完全解决这个问题,但它们创建了第二个独立的评估,有意义地降低了失败率。
后置条件模型在自动化编码中有一个有趣的应用:使用你现有的测试套件作为后置条件。如果测试通过,实现就是正确的。如果它们失败,失败会反馈为错误消息。
from ai_functions import ai_function
from pydantic import BaseModel
from typing import Any, Literal
import pytest, io
from contextlib import redirect_stderr, redirect_stdout
class FeatureRequest(BaseModel):
description: str
test_files: list[str]
# Post-conditions can request original input arguments by name.
# Here, `feature` matches the parameter name of `implement_feature`.
def run_tests(_answer: Any, feature: FeatureRequest):
stdio_capture = io.StringIO()
with redirect_stdout(stdio_capture), redirect_stderr(stdio_capture):
retcode = pytest.main(feature.test_files)
if retcode:
raise RuntimeError(stdio_capture.getvalue())
@ai_function(post_conditions=[run_tests])
def implement_feature(feature: FeatureRequest) -> Literal["done"]:
"""
Implement the following feature in the current code base:
<feature>{feature.description}</feature>
Once done the code base should pass the following tests: {feature.test_files}
"""
def run_workflow(features: list[FeatureRequest]):
for feature in features:
implement_feature(feature)
AI Function 的返回值只是字符串 "done"——这不重要。重要的是副作用:代码库现在应该通过指定的测试。后置条件运行 pytest,如果任何测试失败则抛出异常。LLM 接收测试输出作为反馈,并继续迭代直到所有测试通过。
文档指出,当提供后置条件以及提示指令时,智能体通过的测试大约增加 10-15%。智能体在响应具体验证失败方面的有效性明显更高,而不是跟随书面指令。这符合更广泛的模式:具体的自动化反馈循环胜过详细的提示。这正是 3.1 相对 3.0 的论证。
AI Functions 是一个实验。代码在 strands-labs/ai-functions 开源,是 Strands Labs GitHub 组织的一部分——一个为 Strands Agents SDK 构建的实验项目的家园。除了 AI Functions,你还会找到 Robots(边缘硬件上的物理 AI 智能体)和 Robots Sim(用于机器人开发的模拟环境)。这三个都是基于 Strands Agents SDK 构建的,它已经