文章解读 ICML 2024 的 CodeAct 研究,让模型通过可执行代码表达行动。该方式可以组合工具操作、处理中间结果并控制执行流程,为 Agent 提供更灵活的行动表达方式。
可执行代码动作能够激发更强的 LLM 智能体——探索一种构建更强大 AI 智能体的新方式。
构建 AI 智能体时,我们往往专注于添加更多工具、集成更多 API,以及连接更多 MCP 服务器。
背后的假设很直接:智能体拥有的工具越多,能力就越强。
但如果真正的限制并不是智能体可用的工具数量,而是它使用这些工具的方式呢?
这正是 ICML 2024 上发表的研究论文《可执行代码动作能够激发更强的 LLM 智能体》所探讨的问题。
论文提出了 CodeAct:一种让 LLM 通过可执行代码来表达动作的方法,而不是完全依赖传统的工具调用格式。
智能体可以编写代码,将多个操作组合起来、处理中间结果、控制执行流程,并与执行环境交互,而不只是发起彼此独立的工具调用。
这个想法很简单,但它对智能体式 AI 的意义深远。
📄 阅读原始研究论文
传统的 LLM 智能体通常通过预定义接口与工具交互。
模型选择一个工具,生成参数,接收结果,然后决定下一步做什么。
例如,智能体可能需要获取客户记录、筛选记录并生成报告。
采用工具调用的工作流可能涉及几次独立调用:
{
"tool": "get_customers",
"arguments": {}
}
随后,模型接收结果,并生成另一次工具调用:
{
"tool": "filter_customers",
"arguments": {
"status": "active"
}
}
最后,它可能再调用一个工具来生成报告。
这种方法适用于许多任务。不过,协调多个相互依赖的操作,可能需要模型反复做出决策、传递中间消息,并借助编排逻辑。
CodeAct 提供了另一种可能。
智能体可以将工作流表达为可执行代码:
customers = get_customers()
active_customers = [
customer
for customer in customers
if customer["status"] == "active"
]
report = generate_report(active_customers)
print(report)
上述函数代表环境向智能体提供的工具。
关键区别在于,智能体可以直接用代码表达操作之间的关系。
它可以使用变量、循环、条件、函数和异常处理来协调自己的动作。
CodeAct 将可执行代码视为一种动作格式,而不只是展示给用户的文本。
考虑一个需要从三个服务收集信息的任务。
传统的编排层可能会明确规定操作顺序:
accounts = call_tool("get_accounts")
cards = call_tool("get_cards")
transactions = call_tool("get_transactions")
result = call_tool(
"combine_financial_data",
{
"accounts": accounts,
"cards": cards,
"transactions": transactions
}
)
基于代码的方法可以组合相同的操作,并在执行环境中进行额外的本地处理:
accounts = get_accounts()
cards = get_cards()
transactions = get_transactions()
summary = {
"account_count": len(accounts),
"card_count": len(cards),
"transaction_count": len(transactions),
"total_spending": sum(
transaction["amount"]
for transaction in transactions
if transaction["amount"] > 0
)
}
print(summary)
这个示例假设工具会以预期格式返回数据,并且正数交易金额代表支出。
优势不只是代码行数更少,而是能够动态定义数据流和控制流。
智能体可以自行决定如何处理结果,无须为每一种可能的操作组合都提供一个单独的工具。
flowchart TD
A[User Task] --> B[Traditional Agent]
B --> C[Tool Call 1]
C --> D[Observe Result]
D --> E[Tool Call 2]
E --> F[Observe Result]
F --> G[Tool Call 3]
G --> H[Final Answer]
A --> I[CodeAct-Inspired Agent]
I --> J[Generate Executable Code]
J --> K[Execute Multiple Operations]
K --> L[Observe Execution Result]
L --> M{Task Complete?}
M -- No --> J
M -- Yes --> N[Final Answer]
这张图展示了概念上的区别。实际架构各不相同,传统智能体也可以支持批量调用、循环和多步执行。
并非所有任务都遵循相同的路径。
设想一个获取客户数据的智能体。如果请求成功,它就分析结果;如果请求失败,它就调查错误。
response = fetch_customer_data()
if response["status"] == "success":
customer = response["data"]
analysis = analyze_customer(customer)
print(analysis)
else:
error_details = response.get(
"error",
"Unknown error"
)
investigation = investigate_error(error_details)
print(investigation)
在固定的工作流中,这种分支逻辑可能需要在编排层中定义。
使用可执行代码时,智能体可以直接在生成的动作中表达这个分支。
假设智能体必须检查多个代码仓库或服务。
repositories = [
"frontend",
"backend",
"shared-libraries"
]
results = {}
for repository in repositories:
results[repository] = search_repository(
repository,
query="payment"
)
for repository, matches in results.items():
print(repository, len(matches))
循环可以协调重复操作,同时保留结果,以供后续分析。
这些工具仍然需要在执行环境中可用。CodeAct 不会自动创造新的外部能力。
智能体可能需要先处理某个操作返回的数据,再将其传递给另一个操作。
transactions = get_transactions()
high_value_transactions = [
transaction
for transaction in transactions
if transaction["amount"] >= 10000
]
customer_ids = {
transaction["customer_id"]
for transaction in high_value_transactions
}
customers = [
get_customer(customer_id)
for customer_id in customer_ids
]
print({
"high_value_transactions": len(
high_value_transactions
),
"related_customers": len(customers)
})
在这里,智能体可以通过一个可执行工作流完成记录筛选、标识符提取和关联实体获取。
在真实的金融应用中,这些操作需要适当的授权、隐私控制,以及明确定义的金额语义。
代码还可以让一个动作更容易理解和复用。
def investigate_customer(customer_id):
customer = get_customer(customer_id)
transactions = get_transactions(customer_id)
flagged_transactions = [
transaction
for transaction in transactions
if transaction.get("flagged", False)
]
return {
"customer": customer,
"transaction_count": len(transactions),
"flagged_transactions": flagged_transactions
}
result = investigate_customer("customer-123")
print(result)
当多个操作构成一个逻辑工作单元时,这种方式很有用。
它也带来了一项新的责任:环境必须控制生成的代码可以调用哪些函数。
可执行动作最有意思的特点之一,是能够观察运行时反馈。
智能体不必在生成代码后就停下来。它可以执行代码、检查结果,再根据观察到的信息决定下一步做什么。
生成 → 执行 → 观察 → 修订
来看这个例子。
第一次尝试假设某个字段存在:
response = fetch_customer()
print(response["customer_name"])
假设执行时返回了 KeyError。
智能体可以检查返回的数据结构:
response = fetch_customer()
print("Available fields:", list(response.keys()))
它可能会发现,这个字段实际叫作 name。
下一次尝试就可以适配实际的数据结构:
response = fetch_customer()
customer_name = response.get(
"customer_name",
response.get("name", "Unknown")
)
print(customer_name)
这展示了执行反馈如何指导后续动作。
这并不意味着模型会从每一次错误中永久学到东西。错误会成为一条观察信息,为当前任务的下一步提供依据。
task = "Retrieve and analyze customer data"
while not task_is_complete(task):
code = llm.generate_code(
task=task,
observations=get_observations()
)
result = sandbox.execute(code)
record_observation(result)
if result.error: task = update_task_with_error( task, result.error )
print(get_final_answer())
这是概念性伪代码,并非研究论文中描述的具体实现。
生产系统需要明确的终止条件、执行限制、观测结果处理机制,以及防止无限重试的保护措施。
## 5. 研究人员发现了什么?
论文提出了 M³ToolEval,这是一个用于评估工具使用能力的基准,涵盖需要使用多个工具、进行多轮交互的任务。
研究人员评估了 17 种不同的 LLM,以研究可执行代码动作如何影响智能体的表现。
论文报告的改进幅度最高达到:
- 任务成功率提升 20%
- 在所报告的实验中,动作数量减少 30%
这些是论文报告的最大改进幅度,并不意味着所有模型、基准或任务都能获得同样的收益。
研究结果表明,智能体表示和执行动作的方式,可能显著影响其完成复杂任务的能力。
这里的启示并不只是 Python 比 JSON 更好。
更重要的是,可执行代码提供了灵活的动作空间,让智能体能够协调操作、复用中间结果,并表达条件逻辑。
对于需要执行多个相互依赖的操作的任务,这种灵活性可以减少显式编排的工作量。
## 6. CodeActAgent 和 CodeActInstruct
这项研究还推出了支持这一方法的相关资源。
CodeActAgent 展示了一种智能体架构,通过可执行代码与工具及执行环境交互。
CodeActInstruct 是一个包含约 7,000 条多轮交互的数据集,旨在支持训练模型使用代码作为动作格式。
研究还包含基于 Llama 2 和 Mistral 的模型,探索如何训练模型生成可执行动作,而不是完全依赖传统的工具调用格式。
这很有意义,因为这项工作同时研究了采用这一方法所需的架构和模型行为。
可以在 GitHub 上查看 CodeAct 的官方实现。
## 7. CodeAct 与 MCP:天然的组合?
MCP 提供了一种标准化方式,让 AI 应用能够发现并调用外部服务器提供的工具。
但能够访问大量工具,并不自动意味着智能体可以高效地组合使用它们。
假设一个智能体可以使用以下工具:
search_codebase(query) read_file(path) find_references(symbol) get_file_structure(path)
传统智能体可以逐个调用这些工具。
受 CodeAct 启发的智能体则可以通过程序协调它们:
query = "GET_CREDIT_CARD_DETAIL_DOCUMENT"
matches = search_codebase(query)
investigation = []
for match in matches: file_path = match["file"]
source = read_file(file_path)
investigation.append({ "file": file_path, "match": match, "source_excerpt": source })
print(investigation)
上面的代码假设这些工具返回的数据结构彼此兼容。
需要明确区分:
- MCP 标准化了工具的提供和调用方式。
- CodeAct 探索的是使用可执行代码作为智能体动作的格式。
- 编排框架负责管理执行、状态、权限,以及智能体更完整的生命周期。
这些理念可以相互补充,但不能互相替代。
受 CodeAct 启发的架构可以将 MCP 工具作为构建模块,并生成代码来协调它们。
## 8. 开发者智能体的实际示例:从端点追踪到前端页面
考虑一个银行应用,其架构包含前端、GraphQL、服务于前端的后端(BFF),以及多个微服务。
给定一个服务端点,找出哪些前端页面依赖它。
一种有效的排查方式是沿着以下链路追踪:
REST endpoint ↓ Backend service ↓ BFF service or resolver ↓ GraphQL operation ↓ Frontend hook or data layer ↓ Component ↓ Screen
受 CodeAct 启发的智能体可以组合使用代码仓库搜索工具,追踪这些关系。
endpoint = "/creditcards"
service_matches = search_codebase(endpoint)
graphql_operations = set()
for match in service_matches: operations = find_related_graphql_operations( match["file"], match["line"] )
graphql_operations.update(operations)
screens = set()
for operation in graphql_operations: consumers = find_frontend_consumers(operation)
screens.update(consumers)
print({ "endpoint": endpoint, "graphql_operations": sorted(graphql_operations), "screens": sorted(screens) })
这里的搜索和追踪函数是假设存在的工具,并非 CodeAct 的内置函数。
当结果不完整时,智能体可以进一步展开排查。
例如,它可以搜索某个操作的名称,检查匹配到的文件,然后沿着导入关系追踪到前端。
operation = "GET_CREDIT_CARD_DETAIL_DOCUMENT"
matches = search_codebase(operation)
frontend_files = []
for match in matches: if match["path"].startswith("apps/"): frontend_files.append(match["path"])
elif match["path"].startswith("libs/"): frontend_files.append(match["path"])
unique_files = sorted(set(frontend_files))
print(unique_files)
这个简化示例仅按路径筛选搜索结果。实际实现需要理解代码仓库的结构,区分声明与使用位置,并追踪导入关系和数据依赖。
### 一种可能的排查流程
flowchart TD A[User provides endpoint] --> B[Search repository] B --> C[Find service implementation] C --> D[Trace BFF and resolver] D --> E[Find GraphQL operation] E --> F[Find frontend consumers] F --> G{Evidence complete?} G -- No --> H[Generate another search action] H --> B G -- Yes --> I[Return evidence-backed report]
这展示了一种可能的、受 CodeAct 启发的架构,并非论文中实现或评估过的工作流。
其价值在于,智能体能够动态组合现有的搜索能力,无须为每个端点单独编写硬编码的工作流。
不过,CodeAct 并不能替代代码索引、语义搜索或可靠的代码仓库导航能力。对于大型代码库,这些能力仍然必不可少。
## 9. 并行操作与数据聚合
有些排查任务包含可以并行执行的独立操作。
例如,智能体可以在前端、BFF 和共享库中搜索同一个操作。
import asyncio
async def investigate_repository(repository, query): return await search_repository_async( repository, query )
async def investigate_all(query): repositories = [ "frontend", "bff", "shared-libraries" ]
tasks = [ investigate_repository(repository, query) for repository in repositories ]
results = await asyncio.gather(*tasks)
return { repository: result for repository, result in zip( repositories, results ) }
results = await investigate_all( "GET_CREDIT_CARD_DETAIL_DOCUMENT" )
print(results)
这是一个示意性示例,假设工具函数支持异步调用,且执行环境支持顶层 await。
CodeAct 不会自动让工具调用并行执行。生成的程序、运行时和可用接口都必须支持并发。
当操作相互独立时,并行执行可以降低延迟,但也需要合理的并发限制、错误处理和资源管理。
## 10. CodeAct 会取代传统工具调用吗?
当操作简单、定义明确,并且需要严格校验参数时,传统工具调用仍然很有用。
当任务涉及以下情况时,可执行代码会更具吸引力:
- 多个相互依赖的操作
- 条件执行或重复执行
- 工具之间的数据转换
- 复杂的多步骤排查
混合设计可以对定义明确的操作使用结构化工具调用,在需要组合多个操作时使用基于代码的编排。
account = get_account(account_id)
if account["status"] == "active": transactions = get_transactions(account_id)
result = summarize_transactions( transactions ) else: result = { "message": "Account is not active" }
print(result)
工具层可以保留访问控制和输入校验,而智能体则使用代码来协调任务。
最合适的架构取决于任务、可用工具、模型,以及所需的安全保障。
## 11. 安全挑战
赋予智能体执行代码的能力,会提高它的灵活性,但也会引入风险。
生成的代码不得不受限制地访问宿主机、生产数据库或敏感文件系统。
生产环境中的实现应考虑:
执行时间和内存限制
受限的文件系统和网络访问
明确的工具权限
对敏感操作的校验
高影响操作的审批要求
一个假设的沙箱接口可能如下所示:
result = sandbox.execute( code=generated_code, timeout_seconds=10, network_access=False, filesystem_access="restricted" )
这只是用于说明的 API,并非真实的 CodeAct 函数。实际的安全控制措施取决于执行平台。
核心原则很简单:
让智能体灵活选择完成任务的方式,同时严格限定它被允许执行的操作。
代码执行应当相互隔离、可观测,并受明确的权限约束。
## 12. CodeAct 能解决什么,又不能解决什么
CodeAct 并不能全面解决智能体面临的所有问题。
它不会自动提供:
可靠的代码库索引
正确的工具实现
从以往失败中持续学习的能力
任务完成保证
默认安全的执行环境
这种方法改变了智能体可用的动作空间。结果的质量仍然取决于模型、工具、执行环境以及任务的评估方式。
在决定是否将 CodeAct 应用于实际应用时,明确这一区别非常重要。
## 结论:更强大的智能体可能需要更好的行动方式
CodeAct 的吸引力并不只是让 LLM 能够编写 Python。模型生成代码的能力已经存在多年。
更重要的理念是,将可执行代码作为 AI 智能体的通用动作格式。
我们可以为智能体提供一组能力,让它们将这些能力组合成可执行的工作流,而不必提前设计每一种可能的工具调用序列。
这项研究提供了实验证据,表明这种方法能够提升某些多工具任务的表现,同时减少所需的动作次数。
对于构建 MCP 集成、智能体编排平台以及 AI 驱动的开发工具的开发者而言,CodeAct 提供了一个有价值的架构视角。
也许,构建更强大智能体的下一步,并不只是增加更多工具。
也许,我们需要让智能体更好地使用它们已经拥有的工具。
原始研究论文:可执行代码动作催生更出色的 LLM 智能体
GitHub 上的 CodeAct 实现
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。