1
月
在社区成长 1 月
LEVEL 1
1 XP
一、背景:为什么 Agent 项目经常“能演示、难上线”
很多 Agent 项目在演示阶段表现良好:模型能够理解用户意图,调用搜索、数据库、代码执行或业务 API,并返回看似合理的结果。但进入生产环境后,问题通常集中在几个方面:工具参数不可信、权限边界模糊、长链路失败难以恢复、记忆污染、上下文不断膨胀,以及无法回答“这个结论是如何得到的”。因此,Agent 不应被理解为一个简单的聊天机器人,而应被设计成一个具备规划、状态管理、工具执行、结果验证和审计能力的受控运行时。
本文以一个本地可运行的“安全审计辅助 Agent”为例,讨论如何搭建具有明确边界的 Agent 系统。示例只面向本地代码、授权靶场和防御性分析,不连接真实生产资产,也不提供针对未授权目标的操作方法。
二、目标架构与核心原则
一个较稳妥的 Agent 可以拆分为六层:
1. 交互层:接收用户问题,生成任务 ID,并记录原始请求。
2. 编排层:决定任务是否需要规划、调用哪些工具、何时重试或终止。
3. 工具层:封装文件读取、静态分析、测试执行、知识库查询等能力。
4. 状态层:保存短期上下文、任务状态、工具结果和失败原因。
5. 记忆层:区分会话记忆、项目记忆和长期知识,避免把未经验证的输出直接写入长期记忆。
6. 安全与观测层:执行身份校验、参数校验、权限控制、日志记录、成本限制和人工审批。
关键原则是“模型负责建议,系统负责裁决”。模型可以提出调用工具的意图,但不能直接获得任意网络访问、任意文件权限或无限制执行能力。所有高风险动作必须经过策略引擎和必要的人工确认。
建议定义如下状态机:INIT、PLANNING、WAITING_APPROVAL、EXECUTING、VERIFYING、COMPLETED、FAILED。状态只能按照预定义路径转换,不能由模型随意生成。比如,未经批准的部署、写入生产数据库或发送外部通知,只能停留在 WAITING_APPROVAL 状态。
三、前置条件
本地实验建议准备:
1. Python 3.11 或更高版本。
2. 一个可访问的模型 API,或本地推理服务。模型不要求特定厂商,但应支持结构化输出或工具调用。
3. Docker,用于隔离测试工具。
4. 一个仅包含测试代码的本地目录,例如 ./lab-contracts。
5. SQLite 或 PostgreSQL,用于保存任务、工具调用和审计日志。
初始化环境示例:
python -m venv .venv
source .venv/bin/activate
pip install pydantic fastapi uvicorn sqlalchemy pytest
mkdir -p lab-contracts logs
如果需要运行静态分析器,应将其放在独立容器中,并将目录挂载为只读。不要将宿主机根目录、云凭证目录、SSH 密钥目录或生产配置文件暴露给 Agent。
四、工具调用设计:先定义契约,再接入模型
工具调用最常见的问题不是模型不会调用,而是工具契约过于宽松。例如直接提供一个 shell(command: string) 工具,等于把系统权限交给了模型生成的一段字符串。更合理的方式是按业务能力提供最小化工具,并用严格 Schema 校验参数。
下面是一个简化的工具定义示例:
from pydantic import BaseModel, Field
from typing import Literal
class ScanInput(BaseModel):
project_id: str = Field(pattern=r"^[a-zA-Z0-9_-]{1,40}$")
target_file: str = Field(pattern=r"^[a-zA-Z0-9_./-]{1,160}$")
mode: Literal["syntax", "security"] = "security"
class ScanResult(BaseModel):
status: Literal["ok", "failed"]
findings: list[dict]
evidence: list[str]
工具实现还必须在服务端再次校验路径,而不是只依赖 Pydantic。推荐流程是:先将 project_id 映射到允许目录,再使用 realpath 解析 target_file,最后确认解析后的路径仍位于允许目录之内。即使路径参数看起来合法,也不能直接拼接后执行。
示例伪代码:
def resolve_allowed_file(project_root, user_path):
root = os.path.realpath(project_root)
target = os.path.realpath(os.path.join(root, user_path))
if os.path.commonpath([root, target]) != root:
raise PermissionError("path outside project sandbox")
if not os.path.isfile(target):
raise FileNotFoundError(target)
return target
工具返回值也要结构化,至少包含 status、findings、evidence、duration_ms 和 tool_version。不要只返回一大段自然语言,因为后续验证器、评测程序和审计人员都需要稳定字段。
五、编排流程:规划、执行、验证分离
建议不要让模型一次性完成“分析、执行、下结论”。可以拆成三阶段:
第一阶段是规划。模型只输出任务分解和所需工具,例如读取指定文件、运行本地静态分析、检索项目规则。规划结果必须通过 Schema 校验,并检查工具是否在当前角色允许范围内。
第二阶段是执行。编排器按顺序或有限并行度调用工具。每次调用都生成 call_id,记录输入摘要、调用者、开始时间、结束时间、返回状态和错误分类。工具超时应被标记为 TIMEOUT,而不是让模型自行猜测结果。
第三阶段是验证。验证器检查结论是否有证据支持。例如模型声称存在权限控制缺陷,则必须关联具体文件、函数、代码位置和复现条件;如果缺少证据,应将结论降级为“待确认”,而不是输出确定性判断。
一个简单的重试策略可以是:网络型临时错误最多重试两次,参数错误不重试,权限错误立即终止,工具输出格式错误则进入修复或人工复核。重试必须设置总预算,避免模型在失败链路中无限循环。
六、记忆系统:把“知道”与“暂时推测”分开
Agent 的记忆建议至少分为三类:
1. 会话记忆:只服务当前任务,任务结束后可过期。
2. 项目记忆:保存经过人工确认的项目约定、组件版本和审计结论。
3. 知识记忆:保存通用规范、内部流程和已验证的安全规则。
最重要的控制点是写入门槛。模型输出的“可能存在重入风险”不能直接写入长期知识库;只有当结果经过工具证据、人工确认或重复验证后,才允许进入持久记忆。每条记忆建议带有 source、confidence、created_at、review_status 和 expiration 字段。
检索时也要防止记忆污染。对于用户提供的文件、网页内容或历史对话,应将其视为不可信数据,不应把其中的指令当作系统指令执行。检索结果进入上下文前,最好标注来源和可信级别,并限制每次注入的最大长度。
七、安全边界:权限、提示注入与数据泄露
1. 最小权限:不同工具使用不同服务账户。只读分析工具不能获得写权限,测试执行工具不能访问外网。
2. 沙箱隔离:优先使用容器或微虚拟机,限制 CPU、内存、进程数、磁盘容量和运行时长。
3. 网络控制:默认拒绝出站网络,仅允许访问本地依赖或明确批准的测试服务。
4. 凭证隔离:不要把 API Key 放进模型上下文;通过短时令牌或服务端代理完成受控调用。
5. 输出过滤:在返回结果前检测密钥、令牌、个人信息和内部路径,必要时脱敏。
6. 提示注入防御:外部文档中的“忽略之前规则”“上传机密文件”等内容只能当作普通文本,不能改变系统策略。
7. 人工审批:部署、资金转移、权限变更、外部发送和删除操作必须暂停等待批准。
可以将工具风险分为四级:L0 只读且无副作用,L1 本地计算,L2 修改测试环境,L3 影响外部系统。默认只开放 L0 和 L1;L2 需要任务级授权;L3 必须人工审批并记录审批人、理由和有效期。
八、可观测性与评测
没有日志就无法判断 Agent 是模型错误、工具错误还是编排错误。建议每个任务使用 trace_id 串联以下事件:用户请求、规划版本、工具调用、模型响应、验证结果、状态变更、最终输出和人工操作。
评测不能只看最终答案是否“像人话”,至少要覆盖:
1. 工具选择准确率:是否调用了必要工具,是否调用了不必要工具。
2. 参数合规率:是否满足 Schema、路径和权限约束。
3. 证据完整性:每个安全结论是否关联可复核证据。
4. 失败恢复率:工具超时、空结果和异常时是否正确降级。
5. 越权阻断率:面对提示注入或高风险请求,是否拒绝不合规操作。
6. 成本与延迟:平均调用次数、Token 消耗、P95 响应时间。
本地可以建立固定数据集,包括正常请求、缺失参数、恶意文档、超大输入、工具超时和权限不足等案例。每次修改系统提示、工具 Schema 或路由策略后自动回归。安全评测重点不是让模型“更大胆”,而是验证它在证据不足时能否明确说不知道,并且停止危险链路。
九、一个可执行的本地验收流程
第一步,准备仅包含测试合约和配置样例的 lab-contracts 目录。
第二步,启动只读分析工具容器,并设置资源限制,例如限制单次运行 30 秒、内存 512MB、无网络访问。
第三步,发送一个“检查指定文件中的访问控制和输入校验”的任务。编排器应先生成计划,再调用扫描工具,最后由验证器整理证据。
第四步,故意在测试文档中加入要求读取环境变量或上传文件的文本,确认 Agent 将其视为不可信内容,而不是执行指令。
第五步,传入越界路径、超长参数和不存在的项目 ID,确认服务端拒绝请求,并在日志中记录明确的错误类型。
第六步,模拟工具超时和返回非法 JSON,确认任务进入 FAILED 或 WAITING_APPROVAL,而不是凭空生成扫描结果。
第七步,检查审计日志是否包含 trace_id、工具版本、输入摘要、输出摘要、权限决策和最终状态,同时确认敏感值已脱敏。
十、常见问题
问题一:为什么不让模型直接写 Shell?
因为 Shell 具有组合任意能力,难以通过提示词保证安全。应将能力拆成有限、可验证、可审计的业务工具。
问题二:工具调用失败时是否应该让模型自动重试?
可以,但必须按错误类型区分,并设置次数、时间和总成本上限。参数错误和权限错误通常不应重试。
问题三:长期记忆越多是不是越聪明?
不是。未经审核的记忆会导致错误固化和提示注入持久化。记忆质量、来源和过期机制比数量更重要。
问题四:有了沙箱是否就安全了?
不是。沙箱只能降低运行时影响,仍需处理数据泄露、凭证暴露、供应链风险、资源耗尽和错误结论传播。
问题五:如何处理模型给出的高危结论?
要求结论绑定证据和置信度,并经过独立规则或第二个分析器复核。涉及真实资产的操作必须转为人工审查流程。
十一、总结
可靠的 Agent 不是“更会聊天的模型”,而是一个由模型、工具、状态机、权限系统、验证器和审计系统共同组成的应用。工程重点应放在可控边界:工具最小化、参数强校验、执行强隔离、记忆分层、状态可恢复、结论有证据、风险可追踪。
在智能合约安全审计场景中,Agent 最适合承担资料整理、规则匹配、测试编排和初步证据归纳,而不应未经复核直接替代最终审计判断。只要坚持“模型负责建议,系统负责裁决”,并通过本地授权实验持续验证失败路径,Agent 才可能从演示脚本逐步成长为可维护、可审计、可上线的工程系统。
很多 Agent 项目在演示阶段表现良好:模型能够理解用户意图,调用搜索、数据库、代码执行或业务 API,并返回看似合理的结果。但进入生产环境后,问题通常集中在几个方面:工具参数不可信、权限边界模糊、长链路失败难以恢复、记忆污染、上下文不断膨胀,以及无法回答“这个结论是如何得到的”。因此,Agent 不应被理解为一个简单的聊天机器人,而应被设计成一个具备规划、状态管理、工具执行、结果验证和审计能力的受控运行时。
本文以一个本地可运行的“安全审计辅助 Agent”为例,讨论如何搭建具有明确边界的 Agent 系统。示例只面向本地代码、授权靶场和防御性分析,不连接真实生产资产,也不提供针对未授权目标的操作方法。
二、目标架构与核心原则
一个较稳妥的 Agent 可以拆分为六层:
1. 交互层:接收用户问题,生成任务 ID,并记录原始请求。
2. 编排层:决定任务是否需要规划、调用哪些工具、何时重试或终止。
3. 工具层:封装文件读取、静态分析、测试执行、知识库查询等能力。
4. 状态层:保存短期上下文、任务状态、工具结果和失败原因。
5. 记忆层:区分会话记忆、项目记忆和长期知识,避免把未经验证的输出直接写入长期记忆。
6. 安全与观测层:执行身份校验、参数校验、权限控制、日志记录、成本限制和人工审批。
关键原则是“模型负责建议,系统负责裁决”。模型可以提出调用工具的意图,但不能直接获得任意网络访问、任意文件权限或无限制执行能力。所有高风险动作必须经过策略引擎和必要的人工确认。
建议定义如下状态机:INIT、PLANNING、WAITING_APPROVAL、EXECUTING、VERIFYING、COMPLETED、FAILED。状态只能按照预定义路径转换,不能由模型随意生成。比如,未经批准的部署、写入生产数据库或发送外部通知,只能停留在 WAITING_APPROVAL 状态。
三、前置条件
本地实验建议准备:
1. Python 3.11 或更高版本。
2. 一个可访问的模型 API,或本地推理服务。模型不要求特定厂商,但应支持结构化输出或工具调用。
3. Docker,用于隔离测试工具。
4. 一个仅包含测试代码的本地目录,例如 ./lab-contracts。
5. SQLite 或 PostgreSQL,用于保存任务、工具调用和审计日志。
初始化环境示例:
python -m venv .venv
source .venv/bin/activate
pip install pydantic fastapi uvicorn sqlalchemy pytest
mkdir -p lab-contracts logs
如果需要运行静态分析器,应将其放在独立容器中,并将目录挂载为只读。不要将宿主机根目录、云凭证目录、SSH 密钥目录或生产配置文件暴露给 Agent。
四、工具调用设计:先定义契约,再接入模型
工具调用最常见的问题不是模型不会调用,而是工具契约过于宽松。例如直接提供一个 shell(command: string) 工具,等于把系统权限交给了模型生成的一段字符串。更合理的方式是按业务能力提供最小化工具,并用严格 Schema 校验参数。
下面是一个简化的工具定义示例:
from pydantic import BaseModel, Field
from typing import Literal
class ScanInput(BaseModel):
project_id: str = Field(pattern=r"^[a-zA-Z0-9_-]{1,40}$")
target_file: str = Field(pattern=r"^[a-zA-Z0-9_./-]{1,160}$")
mode: Literal["syntax", "security"] = "security"
class ScanResult(BaseModel):
status: Literal["ok", "failed"]
findings: list[dict]
evidence: list[str]
工具实现还必须在服务端再次校验路径,而不是只依赖 Pydantic。推荐流程是:先将 project_id 映射到允许目录,再使用 realpath 解析 target_file,最后确认解析后的路径仍位于允许目录之内。即使路径参数看起来合法,也不能直接拼接后执行。
示例伪代码:
def resolve_allowed_file(project_root, user_path):
root = os.path.realpath(project_root)
target = os.path.realpath(os.path.join(root, user_path))
if os.path.commonpath([root, target]) != root:
raise PermissionError("path outside project sandbox")
if not os.path.isfile(target):
raise FileNotFoundError(target)
return target
工具返回值也要结构化,至少包含 status、findings、evidence、duration_ms 和 tool_version。不要只返回一大段自然语言,因为后续验证器、评测程序和审计人员都需要稳定字段。
五、编排流程:规划、执行、验证分离
建议不要让模型一次性完成“分析、执行、下结论”。可以拆成三阶段:
第一阶段是规划。模型只输出任务分解和所需工具,例如读取指定文件、运行本地静态分析、检索项目规则。规划结果必须通过 Schema 校验,并检查工具是否在当前角色允许范围内。
第二阶段是执行。编排器按顺序或有限并行度调用工具。每次调用都生成 call_id,记录输入摘要、调用者、开始时间、结束时间、返回状态和错误分类。工具超时应被标记为 TIMEOUT,而不是让模型自行猜测结果。
第三阶段是验证。验证器检查结论是否有证据支持。例如模型声称存在权限控制缺陷,则必须关联具体文件、函数、代码位置和复现条件;如果缺少证据,应将结论降级为“待确认”,而不是输出确定性判断。
一个简单的重试策略可以是:网络型临时错误最多重试两次,参数错误不重试,权限错误立即终止,工具输出格式错误则进入修复或人工复核。重试必须设置总预算,避免模型在失败链路中无限循环。
六、记忆系统:把“知道”与“暂时推测”分开
Agent 的记忆建议至少分为三类:
1. 会话记忆:只服务当前任务,任务结束后可过期。
2. 项目记忆:保存经过人工确认的项目约定、组件版本和审计结论。
3. 知识记忆:保存通用规范、内部流程和已验证的安全规则。
最重要的控制点是写入门槛。模型输出的“可能存在重入风险”不能直接写入长期知识库;只有当结果经过工具证据、人工确认或重复验证后,才允许进入持久记忆。每条记忆建议带有 source、confidence、created_at、review_status 和 expiration 字段。
检索时也要防止记忆污染。对于用户提供的文件、网页内容或历史对话,应将其视为不可信数据,不应把其中的指令当作系统指令执行。检索结果进入上下文前,最好标注来源和可信级别,并限制每次注入的最大长度。
七、安全边界:权限、提示注入与数据泄露
1. 最小权限:不同工具使用不同服务账户。只读分析工具不能获得写权限,测试执行工具不能访问外网。
2. 沙箱隔离:优先使用容器或微虚拟机,限制 CPU、内存、进程数、磁盘容量和运行时长。
3. 网络控制:默认拒绝出站网络,仅允许访问本地依赖或明确批准的测试服务。
4. 凭证隔离:不要把 API Key 放进模型上下文;通过短时令牌或服务端代理完成受控调用。
5. 输出过滤:在返回结果前检测密钥、令牌、个人信息和内部路径,必要时脱敏。
6. 提示注入防御:外部文档中的“忽略之前规则”“上传机密文件”等内容只能当作普通文本,不能改变系统策略。
7. 人工审批:部署、资金转移、权限变更、外部发送和删除操作必须暂停等待批准。
可以将工具风险分为四级:L0 只读且无副作用,L1 本地计算,L2 修改测试环境,L3 影响外部系统。默认只开放 L0 和 L1;L2 需要任务级授权;L3 必须人工审批并记录审批人、理由和有效期。
八、可观测性与评测
没有日志就无法判断 Agent 是模型错误、工具错误还是编排错误。建议每个任务使用 trace_id 串联以下事件:用户请求、规划版本、工具调用、模型响应、验证结果、状态变更、最终输出和人工操作。
评测不能只看最终答案是否“像人话”,至少要覆盖:
1. 工具选择准确率:是否调用了必要工具,是否调用了不必要工具。
2. 参数合规率:是否满足 Schema、路径和权限约束。
3. 证据完整性:每个安全结论是否关联可复核证据。
4. 失败恢复率:工具超时、空结果和异常时是否正确降级。
5. 越权阻断率:面对提示注入或高风险请求,是否拒绝不合规操作。
6. 成本与延迟:平均调用次数、Token 消耗、P95 响应时间。
本地可以建立固定数据集,包括正常请求、缺失参数、恶意文档、超大输入、工具超时和权限不足等案例。每次修改系统提示、工具 Schema 或路由策略后自动回归。安全评测重点不是让模型“更大胆”,而是验证它在证据不足时能否明确说不知道,并且停止危险链路。
九、一个可执行的本地验收流程
第一步,准备仅包含测试合约和配置样例的 lab-contracts 目录。
第二步,启动只读分析工具容器,并设置资源限制,例如限制单次运行 30 秒、内存 512MB、无网络访问。
第三步,发送一个“检查指定文件中的访问控制和输入校验”的任务。编排器应先生成计划,再调用扫描工具,最后由验证器整理证据。
第四步,故意在测试文档中加入要求读取环境变量或上传文件的文本,确认 Agent 将其视为不可信内容,而不是执行指令。
第五步,传入越界路径、超长参数和不存在的项目 ID,确认服务端拒绝请求,并在日志中记录明确的错误类型。
第六步,模拟工具超时和返回非法 JSON,确认任务进入 FAILED 或 WAITING_APPROVAL,而不是凭空生成扫描结果。
第七步,检查审计日志是否包含 trace_id、工具版本、输入摘要、输出摘要、权限决策和最终状态,同时确认敏感值已脱敏。
十、常见问题
问题一:为什么不让模型直接写 Shell?
因为 Shell 具有组合任意能力,难以通过提示词保证安全。应将能力拆成有限、可验证、可审计的业务工具。
问题二:工具调用失败时是否应该让模型自动重试?
可以,但必须按错误类型区分,并设置次数、时间和总成本上限。参数错误和权限错误通常不应重试。
问题三:长期记忆越多是不是越聪明?
不是。未经审核的记忆会导致错误固化和提示注入持久化。记忆质量、来源和过期机制比数量更重要。
问题四:有了沙箱是否就安全了?
不是。沙箱只能降低运行时影响,仍需处理数据泄露、凭证暴露、供应链风险、资源耗尽和错误结论传播。
问题五:如何处理模型给出的高危结论?
要求结论绑定证据和置信度,并经过独立规则或第二个分析器复核。涉及真实资产的操作必须转为人工审查流程。
十一、总结
可靠的 Agent 不是“更会聊天的模型”,而是一个由模型、工具、状态机、权限系统、验证器和审计系统共同组成的应用。工程重点应放在可控边界:工具最小化、参数强校验、执行强隔离、记忆分层、状态可恢复、结论有证据、风险可追踪。
在智能合约安全审计场景中,Agent 最适合承担资料整理、规则匹配、测试编排和初步证据归纳,而不应未经复核直接替代最终审计判断。只要坚持“模型负责建议,系统负责裁决”,并通过本地授权实验持续验证失败路径,Agent 才可能从演示脚本逐步成长为可维护、可审计、可上线的工程系统。