智能体框架的设计:策略制定、沙箱隔离、内存管理以及验证机制。
了解哪些运行时层能将调用工具的大型语言模型转变为可靠的智能体:执行策略、操作系统沙箱机制、持久状态存储、验证功能、上下文控制以及成本考量。
来源: Agent Harness Engineering: The Missing Layer Between an LLM and a Real AI Agent
那些着手构建人工智能智能体的团队通常会从比较各种模型开始:上下文窗口大小、基准测试分数、推理质量以及每个标记的价格。选定模型后,再接入一些工具,最终得到的就是所谓的智能体。实际上此时存在的只是一个能够调用函数的模型,而要让它成为可投入使用的智能体,还需要通过一个运行时层来填补这一差距,这类层现在通常被称为智能体框架。本指南将详细介绍该框架的各个层次、每一层能保护你免受什么风险,以及如何逐步扩展它,同时避免被框架的抽象概念所困扰。
仅能调用工具的模型还不是智能体
典型的起步配置就是一份列有模型可使用的功能的简短清单。
search()
read_file()
write_file()
run_command()
通过这四个函数,模型可以执行搜索、读取、写入和运行命令的操作。这看似赋予了模型自主性,但实际上该架构中并没有任何机制来决定模型可以操作什么内容、哪些操作需要人工确认、崩溃或重启后哪些信息会保留、如何检查输出结果,以及每种工具的产出中有多少会被反馈到提示词中。所有这些职责都由配套框架承担。
此外,这个配套框架本身也是一个可测量的变量。在本文撰写时,一项2026年的研究对比了不同编码智能体配套框架,在每解决一个任务所消耗的标记数上存在高达40倍的差异,而相同框架之间的通过率差距则要小得多。由此可见,聪明的运行时并不会让低性能的模型变得出色,而是能够显著影响高性能模型完成工作的效率、安全性与可预测性。
核心循环及其相关问题
简而言之,智能体就是一个循环:向模型询问该做什么,执行请求的工具,将结果添加到上下文中,然后重复此过程,直到模型表示已完成。
while True:
response = model(context)
if response.is_final:
break
result = execute_tool(response.tool_call)
context.append(result)
许多真实的智能体在其核心机制上都与此类似。模型做出决策,运行时系统执行该决策,观察结果反馈回来,模型再选择下一步行动。循环本身很简单;真正的工程难点在于围绕它构建的所有组件。一个可用的框架必须在每一轮中回答大量操作相关的问题。
Can the agent execute this command?
Can it access this directory?
Does it need human approval?
What happens if the command fails?
What information should return to the model?
What should survive the session?
Should another agent review the result?
How much context should we send?
How much will this turn cost?
What happens if the model loops forever?
综合来看,这些组件共同构成了支撑框架。最近有一篇概念性论文将其定义为通过工具、上下文管理、安全控制、协调机制以及扩展点,将语言模型与外部世界相连的运行时环境。一个便于理解的设想是将该系统分为两个部分。
AI Agent
│
┌────────┴────────┐
│ │
Model Harness
│ │
reasoning ┌──────┼───────┐
planning │ │ │
generation Tools Memory Safety
│ │ │
Sandbox Context Verification
一方面,模型负责提供推理、规划与生成功能;另一方面,支撑框架则提供工具、内存及安全保障,并依托沙箱环境、上下文处理与验证机制。模型赋予系统智能,而支撑框架则掌控着这种智能如何作用于实际系统。
为何提示语不能作为安全机制
假设系统提示语中包含这样一条明确规则:
未经我许可,绝不可删除任何生产文件。
那句话没有任何歧义,但它仍然属于一种请求。模型可能会误解它,在漫长的会话过程中忘记它,将其与上下文中的其他矛盾指令进行权衡,说服自己当前情况属于例外,或者通过规则未明确涵盖的其他方式产生同样的破坏性结果。要求智能体避免某件事,与未经授权就使该事情无法发生有着本质区别。提示语表达的是一种愿望:
Please don't do X.
代码束可以表达在代码中执行的规则:
if dangerous_action and not approved:
deny()
第二个版本无论模型是否记得该指令都能正常工作。这体现了工程设计的第一个原则:只要规则是确定性的,就用确定性代码来实现它。Claude Code 的设计也遵循这一原则。钩子函数会在生命周期事件和工具调用时执行,例如可以在工具调用执行前予以拒绝,而像 CLAUDE.md 这样的文件则提供指导内容,模型会将其作为上下文来读取。如果你在编写此类文件,只需将其用作参考,将具体执行逻辑放到钩子函数中;我们的关于编写高效 CLAUDE.md 文件的实用指南详细说明了该文件应包含的内容。
第一层:请求与操作之间的执行边界
该框架首先需要明确的是智能体可以执行哪些操作。以一个用于删除文件的工具为例。
def delete_file(path):
os.remove(path)
模型可以使用任何它想要的参数来调用该工具。
delete_file("important.md")
如果这种调用立即执行,模型就会对函数所能访问的任何内容拥有完全的控制权。解决办法是通过一个守门机制在调度前检查策略。
def execute(call):
if call.name == "delete_file":
if not call.args.get("human_approved"):
return "DENIED: human approval required"
return dispatch(call)
现在模型仍然可以请求删除操作,但仅仅提出请求并不等同于实际执行。流程变成了一个具有两种可能结果的政策决策过程。
Model decision
↓
Policy check
↓
Permission?
↙ ↘
No Yes
↓ ↓
Deny Execute
这体现了传统的安全设计理念:模型负责提出操作,运行时负责执行。在实际代码中需要进一步完善的细节是审批来源的问题。在上面的示例中,审批标志是从模型生成的调用参数中读取的。而生产环境中的工具应当通过独立渠道获取审批,比如人工确认步骤或模型无法伪造的签名令牌,否则模型就可以直接自行设置该标志。
为什么工具级检查会失效
工具策略虽必不可少,但还不够。假设该工具拒绝了这次调用:
delete_file("secrets/api_key")
这看似起到了保护作用,直到代理程序转而执行等效的shell命令:
rm secrets/api_key
delete_file策略根本无法检测到该操作。或者工具会阻止对敏感路径的读取:
read("secrets/api_key")
但由于没有人对路径进行标准化处理,相对路径便得以绕过限制:
read("../secrets/api_key")
符号链接、代表代理程序打开文件的辅助程序,以及能够执行任意代码的解释器,都会制造出类似的后门。仅通过工具名称和字符串参数来制定策略无法涵盖通往资源的所有路径,因此这并非完整的安全边界。OpenAI关于如何运行编码代理的指导建议是结合沙箱机制、审批策略、受限执行、网络控制以及遥测功能,而非依赖提示词。需牢记的原则是:框架负责执行策略,而操作系统则负责设定最终的安全边界。
第二层:限制代理程序实际可访问的范围
策略规定代理被允许执行哪些操作;而沙箱则首先界定其能够访问的范围。较为弱的方法是使用拒绝列表,试图列出所有敏感位置。
Don't access:
- /etc
- ~/.ssh
- secrets/
- production/
- credentials/
- ...
这样的列表永远无法穷尽。更为强大的方法是反其道而行之:默认几乎不授予任何权限,然后再精确添加任务所需的权限,比如一个项目工作空间。
Agent workspace:
/workspace
├── src/
├── tests/
├── pyproject.toml
└── README.md
代理在主机上仅能接收/workspace,除此之外别无其他内容。用策略来表达的话,这就是一个允许列表。
Allowed:
/workspace
Everything else:
unavailable
由于从智能体的视角来看,文件系统的其他部分根本不存在,因此路径技巧、符号链接以及辅助进程大多失去了作用。容器、专用虚拟机以及受限用户账户是实现这一目标的常见方式。OpenAI的Agents SDK正是为此目的加入了原生沙箱执行功能,这样智能体便能在受控环境中安装依赖项、编辑文件并运行命令。
网络与资源限制也应置于沙箱中
仅仅隔离文件系统还不够。如果智能体能够运行如此简单的操作,其带来的威胁程度就会大不相同:
curl https://example.com
具备出站网络访问权限的智能体可以:
- 下载并运行任意软件
- 将数据泄露到外部主机
- 调用第三方API
因此,名副其实的沙箱需要覆盖多个层面,而不仅仅是文件。
Filesystem
Network
Processes
Environment variables
Secrets
CPU
Memory
Time
进程会默默继承环境变量和机密信息,而CPU、内存和时间限制则能防止构建过程失控。正因如此,现代运行时将沙箱设计为完整的执行环境,而非简单的壳层包装。
第3层:会跨会话持续存在的状态
在长时间运行的任务中会出现不同的问题。某个脚本可能花费一小时来分析代码库,并从中总结出一条重要的架构规则。
The authentication service must never directly access the database.
All database operations go through Repository classes.
此时上下文窗口会被填满,会话将重新启动,那些知识也就随之消失。新的会话根本不知道设计为何是那样的,还可能随意违反这些设计规则。对话历史记录并非存储这类信息的合适场所。一个有效的框架至少会将具有不同生命周期的三种信息分类存放。
Conversation state
↓
temporary
Project state
↓
persistent
Harness configuration
↓
persistent + globally enforced
每种类型的具体示例:
Conversation:
"Let's investigate the login bug."
Project:
"Authentication uses OAuth."
Harness:
"Never modify generated migrations."
当任务结束时,对话内容可以安全地消失。而项目相关信息则应跨会话保持不变,例如存储在带版本控制的笔记文件或内存存储中。无论模型当前记住了什么,都应在每次运行时应用相应规则,这意味着这些规则应存在于配置和执行代码中而非文本文件里。随着会话时间的延长,这种分离显得愈发重要。例如,OpenAI的Agents API就包含了针对长时间运行的智能体的上下文管理功能,其中包括在会话接近上下文限制时自动进行数据压缩。而正是这种压缩操作会导致未记录的项目信息丢失,因此有必要在之前刻意将它们保存下来。如需更深入地了解内存层级结构,请参阅基于LangGraph和Amazon Bedrock的四层智能体内存设计。
LangGraph和Bedrock。第4层:验证智能体并未拥有该内容
智能体完全有可能犯错,因此做出更改的智能体不应成为判断其是否正确的最终依据。以以下输出为例:
def calculate_total(items):
return sum(item.price for item in items) + 1
那些多余的+ 1在编写代码的模型看来可能是有意为之。通过独立的验证步骤,可以根据多个信号来评估这些更改。
Code
Tests
Type checking
Linting
Security rules
Expected behavior
其价值在于将内容生成与判断功能分开。
Agent
│
↓
Changes
│
↓
Verification
↙ ↘
PASS FAIL
↓ ↓
Continue Investigate
一次测试通过则让循环继续;测试失败则会要求智能体重新调查。有一个容易被忽视的改进点:审核者应当报告问题,而非悄悄修复它们。如果验证者默默地解决了错误,就会失去原始智能体出现故障的信号,从而导致成功指标变得不真实。
高风险更改的默认操作为模拟运行
对于风险更高的操作,应将预览默认行为作为标准。而非使用会立即执行的应用函数:
apply_change()
应提供默认安全选项为模拟运行的功能:
apply_change(dry_run=True)
代理首先会收到关于该操作影响的描述。
Would modify:
src/auth.py
tests/test_auth.py
只有通过明确的单独步骤,最好是需要审批的步骤,才能从预览状态转为执行状态。这样在任何更改发生之前,验证者和人工审核者都有具体的内容可以检查。
第5层:上下文工程
放入上下文中的每个标记都会产生成本,会争夺模型的注意力,在处理长任务时往往积累的速度快于被清除的速度。假设智能体需要从一个包含5,000行的文件中获取某个函数。简单的实现方式是先发送整个文件,然后再发送下一次搜索的完整结果,接着是下一个工具的输出,以此类推。不久之后,上下文就会包含所有这些内容:
Task
+ entire repository
+ previous searches
+ tool output
+ logs
+ previous reasoning
+ repeated instructions
从技术上讲,模型可以查看每一个标记,但能够看到并不等同于理解。更好的设计应将上下文视为一个在信息到达主智能体之前对其进行压缩的管道。
Main Agent
│
├── Search Agent
│ │
│ └── 50,000 tokens
│
│ ↓
│
└── 1,000-token summary
在这里,搜索智能体在探索过程中可能会消耗50,000个标记,但主会话仅能收到包含关键信息的1,000个标记的摘要。这充分体现了子智能体的必要性。例如,Claude Code将子智能体描述为在独立的上下文窗口中运行,仅向主对话返回最终结果,从而避免大量的次要查询挤占主要上下文空间。不过这样的权衡意味着摘要可能会遗漏主智能体日后需要的细节,因此摘要应包含指向具体内容的线索(如文件路径、行号范围),以便智能体按需获取详细信息。
经济性考量:每个标记的价格并不等同于每项任务的成本
上下文管理同样涉及成本问题。回想一下那项研究,它发现不同框架在每完成一个任务所需的标记数上可能存在高达40倍的差异,而通过率方面的差异则小得多。这就决定了应该提出什么样的问题。与其问“哪个模型最便宜?”,不如去了解整个智能体系统完成一个任务需要多少标记数,进而对比两种配置。
智能体A:更便宜的模型,但框架效率低下
Model:
$X / million tokens
Task:
500k tokens
智能体B:更昂贵的模型,但框架高效
Model:
2X / million tokens
Task:
100k tokens
代理B每处理一个令牌的花费是代理A的两倍,但使用的令牌数量仅为代理A的五分之一,因此在不考虑成功率差异的情况下,每完成一项任务的成本大约仅为代理A的40%。模型定价只是一个输入参数;而代理的实际成本则取决于该框架让模型执行了多少工作。我们的以架构为导向的代理型AI成本模型对此进行了更深入的分析。
无操作轮次与终止策略
部分浪费在按请求统计的指标中是看不出来的。想象一下,有一个代理一直只是在宣布自己的意图而并未采取任何行动:
I should inspect the repository.
在下一轮次中也是如此:
I should inspect the repository.
再接下来也是这样:
I should inspect the repository.
虽然没有任何变化,但每次轮次都会消耗令牌、增加延迟、占用计算资源与上下文空间,甚至还可能产生费用。相关研究将这种开销描述为“每任务等待税”,而不仅仅是令牌税——用户不得不等待那些毫无成效的轮次。该系统需要终止规则,最简单的方法就是为轮次设置上限。
MAX_TURNS = 30
if agent.turns >= MAX_TURNS:
stop()
上限仅是一种安全保障,并非有效策略。更实用的系统能够识别出表明运行陷入停滞的信号。
No progress
Repeated tool calls
Repeated identical observations
No state change
Test suite unchanged
Agent oscillation
Budget exhausted
重复执行完全相同的工具调用、观测结果毫无变化、文件系统没有变动、测试套件始终以相同方式失败、在两种状态之间振荡,或是预算已耗尽——所有这些情况都表明继续执行很可能无法带来帮助。此时,该框架应当停止运行、进行总结并上报问题。我们的TypeScript中有限代理循环指南通过代码展示了这些模式。
从工具封装器到平台
编码框架设计正在逐渐形成一门独立的学科。一项针对11个实际应用的编码代理系统源代码的最新研究发现,这些系统中存在重复出现的架构模式:工具执行、上下文管理、安全控制、协调机制以及扩展接口。研究作者认为,编码框架已从简单的工具发展成了完整的平台。早期的代理系统大致如下:
LLM + tools
当前的系统类似于堆栈结构。
LLM
↓
Agent loop
↓
Tool router
↓
Policy engine
↓
Sandbox
↓
Context manager
↓
Memory
↓
Verification
↓
Observability
↓
Orchestration
模型依然是核心,但围绕它的运行时环境——从工具路由器、策略引擎到沙箱、上下文管理器、内存管理、验证机制、可观测性工具以及协调组件——已经发展成了独立的产物。
编码代理的参考架构
对于编码智能体而言,一个合理的初始架构是将智能体循环直接置于模型之下,并将其分为三个部分:工具路由器、上下文管理器和内存。工具调用会通过策略引擎进入沙箱,结果则要经过验证,所有操作都会被可观测性机制记录下来。
┌──────────────┐
│ Model │
└──────┬───────┘
│
↓
┌─────────────────┐
│ Agent Loop │
└────────┬────────┘
│
┌─────────────┼─────────────┐
↓ ↓ ↓
Tool Router Context Memory
│ Manager │
↓ │ ↓
Policy Engine │ Persistent State
│ │
↓ ↓
Sandbox ←────── Tools
│
↓
Verification
│
↓
Observability
要让每个模块都能独立测试:使用模拟调用对策略引擎进行单元测试,对路径处理进行模糊测试,并在已知有问题的代码片段上评估验证器的性能,且整个过程都不需要调用模型。
从故障中完善工具集,而非依赖框架
要获得这些好处并不需要庞大的智能体框架。即便加入了策略引擎、沙箱环境和验证功能,核心的循环结构依然可以保持简洁。
while turns < MAX_TURNS:
action = model.next_action(context)
if action.is_final:
return action.answer
if not policy.allows(action):
context.append("Action denied")
continue
result = sandbox.execute(action)
verification = verifier.check(result)
context.append(
summarize(result, verification)
)
注意执行顺序:策略检查在执行之前进行,被拒绝的操作会反馈给模型而非导致循环崩溃,执行过程始终会在沙箱环境中进行,重新进入上下文的是结果及其验证的摘要,而非原始输出。此后,只有在检测到故障时才增加复杂性。
Agent deleted something?
→ add execution policy
Agent accessed the wrong file?
→ strengthen sandbox
Agent forgets architecture?
→ add persistent project memory
Agent produces bad code?
→ add verification
Agent becomes expensive?
→ optimize context
Agent loops?
→ add termination detection
每个症状都对应唯一的一层结构。这比在尚未明确问题所在时就引入五十种抽象层次要合理得多。关于是自行构建此类循环还是采用现有框架的更多讨论,可参阅runtime harnesses vs composed frameworks。
仓库作为智能体的知识库
Hooks:模型无需记忆的行为
钩子是最有用的工具箱原语之一。它能让运行时通过确定性代码对生命周期事件作出响应,其中最常见的是用于控制工具调用的触发。
Before tool call
↓
Check policy
↓
Allow / deny
另一种钩子则能在每次代码修改后保持代码质量的一致性。
After file edit
↓
Run formatter
↓
Run type checker
第三种钩子可在上下文被压缩之前保护相关知识。
Before context compaction
↓
Persist important state
第四种钩子用于构建审计追踪记录。
After task completion
↓
Generate audit record
Claude Code将钩子描述为由生命周期触发的确定性命令,这类命令不仅能执行特定操作,还能在工具调用执行前加以阻止。其优势在于将相关行为完全从模型的推理过程中分离出来——无需在提示词中写“请记得运行格式化工具”,运行时就会自动每次都执行格式化操作。
绝不能让模型掌控边界
模型具有概率性,而安全边界则不应如此。这种反模式将决策权交给了模型:
if model_says_safe:
execute()
这种模式则将决策权交给了那些无法通过沟通改变其答案的基础设施:
if operating_system_allows:
execute()
对于需要判断力的决策,使用模型;而对于需要确定性的决策,则使用确定性基础设施。实际上,每一层都负责回答属于自己的问题。
Model:
Should I modify this file?
Harness:
Is this file inside the workspace?
OS:
Can this process write to the file?
Human:
Should this high-risk action proceed?
模型决定某项变更是否可行,管理工具检查目标是否在工作区域内,操作系统决定进程是否可以写入文件,而人类则决定是否应该执行高风险操作。这样的分层处理方式比期望某个单一组件做到完美要可靠得多。
管理工具是模型的增强手段,而非替代品
人们很容易得出结论:框架比模型更重要。但证据并不支持这一观点。在所研究的场景中,框架的选择对令牌效率有显著影响,但对任务完成率的影响却很小;再多的运行时优化也无法将一个薄弱的模型变成出色的软件工程师。区分两者的简单方法如下:
Model capability
=
What the agent can reason about
Harness quality
=
How effectively that capability is deployed
一个出色的模型如果搭配糟糕的运行时,会浪费大量资源与资金。而围绕薄弱模型精心设计的运行时,虽然能更安全高效地运行,但决策质量依然不佳。两者缺一不可。
部署任何智能体之前需问的五个问题
1. 它能做什么?
列出所有工具,并标记出每一项危险操作。
read
write
delete
execute
network
deploy
send_message
2. 它能访问什么?
工具定义只能说明部分情况,还需检查该进程实际能访问的内容:
filesystem
network
environment
secrets
processes
credentials
3. 重启后什么会保留?
重启代理并查看结果:
What does it remember?
What does it forget?
Where is important state stored?
4. 谁来验证结果?
对于有重要影响的操作,两步流程是不够的:
generate → approve
在生成与批准之间加入独立的检查步骤:
generate
↓
test
↓
review
↓
approve
5. 每项任务的实际成本是多少?
仅依据每次请求的令牌数量来判断是具有误导性的:
tokens per request
应衡量实际输出结果:
tokens per successful task
latency per successful task
tool calls per task
retries per task
human interventions
每项成功任务所消耗的令牌数、延迟时间、工具调用次数、重试次数以及人工干预次数,这些数据才能反映该系统是否值得运行。
生产级代理的实用定义
将各层要素结合在一起,才能得到更准确的定义。
Agent =
Model
+ Tools
+ Execution Environment
+ Policy
+ Memory
+ Context Management
+ Verification
+ Observability
每个要素都有其存在的意义。没有模型就没有智能;没有工具就无法采取行动;没有执行环境与策略,智能体可能会带来危险;没有内存,长时间的任务就会变得不稳定;没有上下文管理,成本会不断上升;没有验证机制,错误就会悄无声息地蔓延;而缺乏可观测性,一旦出错就无从追溯发生了什么。
总结
行业讨论的重点正从哪种模型能在基准测试中表现最佳,转向哪种运行时环境能让模型可靠地完成有用的工作。各供应商都在采用相同的构建模块:OpenAI现在出售集成了沙箱环境、上下文处理、工具搜索、通过代码发起的工具调用以及长时间运行的管理型代理基础设施,而Anthropic则提供了持久化指令、技能、子代理以及确定性钩子功能。研究开始将这种工具框架视为实验变量,而非将模型视为整个系统。这些功能发展迅速,因此请查阅最新文档。
构建代理的简单方法如下:
Pick LLM
+
Add tools
=
Agent
实际的操作方式则是这样:
Model
↓
Agent Runtime
↓
┌─────────┼─────────┐
↓ ↓ ↓
Policy Context Memory
↓ ↓ ↓
Sandbox Tools Persistence
└─────────┼─────────┘
↓
Verification
↓
Observability
模型负责做出决策,而框架则规定了这些决策在何种条件下会转化为实际行动。在选择框架之前,先思考当模型出错时会发生什么,并根据答案来构建相应的框架。
- 在代码和钩子中实施确定性规则,而非依赖提示语。
- 将工具策略视为第一道防线,而操作系统沙箱(包括网络和资源限制)才是真正的边界。
- 按生命周期区分对话状态、项目状态和框架状态,并在数据压缩前保存项目相关信息。
- 保持验证过程的独立性,让审查者负责报告问题而非直接修复。
- 优化每个成功任务所需的令牌数量,停止那些不再取得进展的运行过程。
目标并非打造一个从不出错的智能体,而是构建一个即便模型出现错误也不会自动引发生产事故的系统。