外观
Agent 工程化与安全
目录
- 1. 学习目标
- 2. 面试结论
- 3. 面试官为什么问
- 4. 概念与边界
- 5. 原理剖析
- 6. 实现与代码
- 7. 实际项目案例
- 8. 方案权衡与常见误区
- 9. 面试题与参考答案
- 10. 递进追问
- 11. 实践任务
- 12. 相关知识与参考资料
- 13. 简明总结
1. 学习目标
- 将 Demo Agent 改造成可恢复、可审计、可限权的生产系统;
- 理解超时、重试、幂等、检查点和补偿的不同职责;
- 能设计工具权限、人工批准、输入输出 Guardrail 和敏感数据策略;
- 能用 Trace、Metric、Log 和业务事件定位 Agent 失败;
- 能解释提示注入、过度代理、记忆污染和重复副作用的解决方案。
2. 面试结论
2.1 30 秒回答
Agent 工程化的本质,是把模型的不确定性隔离在可控决策层,把状态、权限、预算、幂等、重试、审计和验收放在确定性运行时。生产系统必须假设模型会选错工具、外部调用会超时、回调会重复、进程会重启、输入可能恶意;因此每个副作用都要经过服务端校验和最小权限,每一步都能恢复和追踪。
2.2 一分钟复述版
一个可靠 Agent 至少需要五类保护:第一,执行保护,包括 Schema、白名单、权限、预算和人工确认;第二,可靠性,包括超时、错误分类、幂等、检查点和补偿;第三,状态保护,把工作状态持久化而不是只存在上下文;第四,安全保护,把外部数据视为不可信内容,防止提示注入和敏感信息泄露;第五,可观测和评测,记录模型、Prompt、工具、Guardrail、状态迁移和最终结果。模型可以建议动作,但不能绕过这些运行时规则。
3. 面试官为什么问
- 是否经历过 Demo 到生产的差距;
- 是否理解分布式系统中的至少一次执行和副作用风险;
- 是否能把“安全 Prompt”升级为权限和策略系统;
- 是否会区分模型错误、工具错误、业务错误和基础设施错误;
- 是否能建立故障证据,而不是靠重试掩盖问题。
4. 概念与边界
4.0 小白先这样理解:Agent 办事要像网银转账
你在网银输入“给房东转 5000 元”,系统不会因为备注里写着“我已获授权”就放款:它先核验登录身份和收款对象,再展示金额让你复核,用唯一业务流水号防止连点两次;若提交后网络断开,先查银行账本确认成败,而不是盲目再转。这里,身份与账户校验是授权,复核页是人工确认,流水号是幂等键,账本查询是对账,电子回单是审计记录,断点记录是检查点。
类比边界: 人工确认不等于权限校验,幂等也不等于跨系统事务或自动回滚;已经成功的外部副作用可能只能补偿。Prompt 和 Guardrail 只能帮助识别风险,真正的权限、状态条件、批准指纹、密钥隔离与执行审计必须由可信服务端强制执行。
4.1 工程化保护面
| 层次 | 保护对象 | 典型机制 |
|---|---|---|
| 输入 | 用户、检索和外部内容 | 类型校验、注入检测、数据分级 |
| 决策 | 模型输出 | 结构化动作、允许列表、预算和策略 |
| 工具 | 真实副作用 | 身份鉴权、最小权限、批准、幂等 |
| 状态 | 长任务与恢复 | 持久化、版本、检查点、事件历史 |
| 输出 | 用户和下游系统 | Schema、事实引用、安全和合规校验 |
| 观测 | 整条执行链 | Trace、Metric、Log、审计和成本 |
4.2 Guardrail 不等于权限
Guardrail 可以检查输入、输出或工具参数,但真正的权限必须由工具服务端根据用户身份、资源范围和当前状态重新判断。模型生成的“已获得授权”不能作为授权证据。
4.3 重试、幂等与补偿
- 重试:再次尝试一个可恢复失败;
- 幂等:同一逻辑请求执行多次,业务结果仍等同一次;
- 补偿:无法原子回滚跨系统操作时,执行反向业务动作;
- 检查点:保存可恢复位置和必要状态;
- 对账:本地状态不确定时,查询外部系统的真实结果。
重试不能替代幂等,幂等也不能保证跨系统强事务。
4.4 安全边界
- 系统指令、用户输入、检索内容、工具结果和外部网页必须分层;
- 外部内容是数据,不是更高优先级指令;
- 工具令牌和数据库凭据不进入模型上下文;
- 不可逆动作默认要求显式用户确认;
- 长期记忆写入需要来源、用途、TTL 和删除能力。
5. 原理剖析
5.1 失败分类
| 类型 | 示例 | 是否重试 | 正确动作 |
|---|---|---|---|
| 瞬时基础设施错误 | 网络抖动、临时 5xx | 有上限地重试 | 指数退避、抖动、对账 |
| 限流 | 429、并发配额 | 延迟重试 | 读取 Retry-After、背压 |
| 参数错误 | Schema 不合法 | 不原样重试 | 修复参数或回到模型 |
| 权限错误 | 越权、令牌无权限 | 不重试 | 拒绝并审计 |
| 业务冲突 | 资源状态已变化 | 先对账 | 重新规划或请求用户 |
| 不确定结果 | 外部成功但本地超时 | 不能直接重做 | 用幂等键查询真实状态 |
| 安全策略命中 | 注入、敏感数据 | 不重试 | 阻断、降权或人工审核 |
5.2 幂等键与状态版本
幂等键应代表一次业务意图,而不是一次网络请求:
工具执行前写入唯一记录,状态从 PENDING 条件更新到 RUNNING,完成后保存外部结果。收到重复调用时返回已有结果或对账,不能再次制造副作用。
5.3 人工确认
确认请求应展示:
- 即将执行的动作和对象;
- 关键参数与预估影响;
- 是否可撤销;
- 数据或费用范围;
- 确认有效期和动作指纹。
批准必须绑定动作指纹。模型在用户批准后修改参数,应重新确认。
5.4 状态持久化与恢复
模型上下文不是可靠状态库。恢复至少需要:
- 工作流 ID、当前步骤和状态版本;
- 已完成工具调用及幂等键;
- 待确认动作和确认结果;
- 预算消耗、重试次数和终止原因;
- 必要输入摘要及原始证据位置;
- 模型、Prompt、工具和策略版本。
5.5 可观测性
- Trace:一次 Agent Run 的模型调用、工具、Handoff、Guardrail 和自定义步骤;
- Metric:成功率、阶段耗时、工具错误率、循环步数、Token 和成本;
- Log:结构化状态迁移、错误和审计事件;
- Artifact:模型输入输出、检索证据、工具响应和评测结果的受控存档。
不要把 user_id、run_id 等高基数字段无限制写入指标标签;它们适合 Trace 和日志检索。
5.6 可视化辅助
工程化保护面不依赖某一个 Agent 框架。本文选择的参考栈把授权、幂等和观测放在模型之外;真实项目可替换具体策略引擎、数据库和采集后端,但不能删除这些职责。
技术清单
| 技术点 ID | 技术点/环节 | 类型 | 采用方案 | 链路职责 | 版本/证据边界 |
|---|---|---|---|---|---|
| TP-SEC-POLICY | 工具授权与策略 | 服务/策略组件 | 服务端 RBAC/ABAC;复杂组织可接 OPA 或 Cedar | 基于真实身份、资源、动作和环境重新授权,不信任模型声称 | 产品语法与能力随版本变化;本文只确认授权必须在可信执行边界完成 |
| TP-SEC-IDEMPOTENCY | 幂等、状态与对账 | 存储/中间件 | 关系数据库唯一业务意图键 + 状态版本 + Outbox/执行记录 | 原子占位、阻止并发重复副作用,并保存外部任务 ID 与不确定态 | 不能承诺跨供应商 exactly-once;必须结合供应商查询、账单和补偿能力 |
| TP-SEC-OBSERVABILITY | Trace、Metric、Log 与审计 | 可观测组件 | OpenTelemetry Trace + 结构化审计事件 + 低基数 Metric | 关联模型、Tool Call、批准、状态迁移、错误、版本和费用 | OTel 负责传播与采集语义,不自动解决敏感信息脱敏、留存和告警质量 |
横向选型对比
| 技术点 ID | 候选方案 | 优点 | 缺点/代价 | 适用场景 | 不适用场景 | 选择结论与依据 |
|---|---|---|---|---|---|---|
| TP-SEC-POLICY | 业务服务内 RBAC/ABAC | 距离业务状态近,调试和强制执行直接 | 多服务容易复制规则并产生漂移 | 服务数量少、资源模型稳定、团队边界清晰 | 多语言多服务且策略需要统一审计 | 默认起点;权限测试必须覆盖资源级越权和状态变化 |
| TP-SEC-POLICY | OPA 或 Cedar 等独立策略引擎 | 策略集中、可版本化并支持跨服务复用 | 引入策略语言、分发、一致性和故障降级成本 | 多租户、多服务、合规审计和统一治理 | 单体小系统或团队无策略运维能力 | 复杂组织候选;以决策延迟、可用性和策略回归集验收 |
| TP-SEC-IDEMPOTENCY | 关系数据库唯一键 + 状态机 | 事务、条件更新、对账与审计证据完整 | 热点写入、清理和 Schema 演进有成本 | 支付、消息、部署等需要恢复的副作用 | 极高吞吐且允许最终一致去重的短事件 | 默认选择;业务意图、结果和状态放在同一可信事实源 |
| TP-SEC-IDEMPOTENCY | Redis SET NX + TTL | 延迟低、实现轻量、适合短窗口抑制重复 | TTL 过期、故障切换和持久化边界可能导致重复 | 可容忍少量重复、短时间防抖和限流 | 财务、删除、跨小时恢复和强审计场景 | 只作前置去重或加速层,不作为高风险最终账本 |
| TP-SEC-OBSERVABILITY | OpenTelemetry + 审计事件 | 跨模型、工具和服务统一 Trace,上下文传播标准化 | 采样、存储、脱敏和成本治理复杂 | 多阶段 Agent、异步 Worker、需要版本归因 | 极简本地实验或无后端采集环境 | 生产默认候选;高风险动作额外保留不可抵赖审计记录 |
| TP-SEC-OBSERVABILITY | 仅应用日志 | 接入快、排查单进程错误方便 | 跨服务关联弱,指标与调用关系需人工拼接 | 单进程原型和早期故障定位 | 多租户、异步链路、发布归因和合规审计 | 只用于最小原型,不能满足生产 Agent 全链路证据需求 |
图:架构|受控 Agent 的安全、状态与观测组件边界
替代文本: 用户身份和请求先进入 Agent Runtime;Runtime 调用模型取得候选动作,但工具动作必须依次经过 Schema、服务端策略与高风险人工批准。幂等状态库在执行前原子占位,工具网关使用短期凭据访问外部系统;OpenTelemetry 和审计存储旁路记录模型、批准、状态与结果,恢复器针对超时不确定态发起对账。
图表加载中…
读图结论: 模型、策略、状态、执行和观测必须分层;服务端授权与幂等账本决定动作能否安全发生,Prompt Guardrail 只能提供额外信号。
架构中的策略引擎、状态库和观测后端都可替换,但真实身份、动作指纹和外部结果不能只保存在模型上下文中。
图:技术调用流程|高风险工具调用的批准、执行与不确定态对账
替代文本: Agent Runtime 收到模型动作后先请求服务端策略;策略拒绝则记录并结束该动作,高风险动作需用户批准。通过后状态库以幂等键原子占位:重复请求返回已有状态,新请求才调用外部工具。成功时持久化结果;超时不确定时先按外部任务 ID 对账,确认未执行才安全重试,已执行则回填成功,无法确认则转人工处理。
图表加载中…
读图结论: 工具超时不是“直接再调用一次”的信号;安全链路必须先冻结重复提交并对账,再根据权威状态决定成功、重试或人工补偿。
该时序把成功、明确失败、策略拒绝、用户拒绝和结果不确定分开,便于为每一类结果配置不同告警、Runbook 和回归测试。
图:高风险工具调用的可靠执行路径
图表加载中…
替代文本: 工具调用先经策略和人工确认,执行不确定时先对账,再决定返回成功或重试。
读图结论: 高风险 Agent 的关键路径不是“调用—失败—重试”,而是“校验—授权—幂等执行—不确定结果对账”。
6. 实现与代码
6.1 最小可运行受控执行器
下面只演示进程内的控制模式:认证上下文不来自模型动作,批准绑定完整动作指纹,资源级授权发生在工具前,并用原子占位阻止同一调用并发执行。它不是数据库或外部供应商的 exactly-once 实现。
python
from dataclasses import dataclass
from hashlib import sha256
import json
from threading import Lock
from typing import Any, Callable
@dataclass(frozen=True)
class AuthContext:
# 真实系统只能由认证中间件根据已验证令牌创建,模型不能填写这些字段。
actor_id: str
roles: frozenset[str]
allowed_profile_ids: frozenset[str]
@dataclass(frozen=True)
class Action:
# call_id 由服务端按一次业务意图签发。
call_id: str
tool: str
args: dict[str, Any]
@dataclass(frozen=True)
class Approval:
call_id: str
actor_id: str
action_fingerprint: str
@dataclass(frozen=True)
class ToolPolicy:
allowed_roles: frozenset[str]
needs_approval: bool
authorize: Callable[[AuthContext, dict[str, Any]], bool]
handler: Callable[..., Any]
@dataclass
class ExecutionRecord:
action_fingerprint: str
status: str
result: Any = None
PROFILES = {"u-1": "active"}
def update_profile_status(user_id: str, status: str) -> dict[str, str]:
if status not in {"active", "suspended"}:
raise ValueError("unsupported status")
PROFILES[user_id] = status
return {"user_id": user_id, "status": status}
def can_manage_profile(auth: AuthContext, args: dict[str, Any]) -> bool:
return (
"admin" in auth.roles
and args.get("user_id") in auth.allowed_profile_ids
)
POLICIES = {
"update_profile_status": ToolPolicy(
allowed_roles=frozenset({"admin"}),
needs_approval=True,
authorize=can_manage_profile,
handler=update_profile_status,
)
}
APPROVALS: dict[str, Approval] = {} # 仅由可信人工批准接口写入
RECORDS: dict[str, ExecutionRecord] = {}
RECORDS_LOCK = Lock()
def fingerprint(auth: AuthContext, action: Action) -> str:
canonical = json.dumps(
{
"actor_id": auth.actor_id,
"call_id": action.call_id,
"tool": action.tool,
"args": action.args,
},
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return sha256(canonical.encode("utf-8")).hexdigest()
def record_human_approval(auth: AuthContext, action: Action) -> Approval:
approval = Approval(action.call_id, auth.actor_id, fingerprint(auth, action))
with RECORDS_LOCK:
APPROVALS[action.call_id] = approval
return approval
def execute(auth: AuthContext, action: Action) -> Any:
policy = POLICIES.get(action.tool)
if policy is None:
raise PermissionError("tool is not allow-listed")
if not (auth.roles & policy.allowed_roles):
raise PermissionError("trusted role is not allowed")
if not policy.authorize(auth, action.args):
raise PermissionError("resource is outside actor scope")
action_fingerprint = fingerprint(auth, action)
# 结果命中、批准消费和执行占位使用同一临界区。
with RECORDS_LOCK:
record = RECORDS.get(action.call_id)
if record is not None:
if record.action_fingerprint != action_fingerprint:
raise ValueError("call_id reused with different intent")
if record.status == "SUCCEEDED":
return record.result
raise RuntimeError(
f"execution status={record.status}; reconcile instead of resubmitting"
)
if policy.needs_approval:
approval = APPROVALS.get(action.call_id)
if (
approval is None
or approval.call_id != action.call_id
or approval.actor_id != auth.actor_id
or approval.action_fingerprint != action_fingerprint
):
raise PermissionError("approval does not match exact action")
del APPROVALS[action.call_id] # 一次性批准与执行占位原子绑定
RECORDS[action.call_id] = ExecutionRecord(
action_fingerprint=action_fingerprint,
status="RUNNING",
)
try:
result = policy.handler(**action.args)
except Exception:
# 不知道外部副作用是否已经发生时,禁止自动重新提交。
with RECORDS_LOCK:
RECORDS[action.call_id].status = "SUBMISSION_UNKNOWN"
raise
with RECORDS_LOCK:
RECORDS[action.call_id].status = "SUCCEEDED"
RECORDS[action.call_id].result = result
return result
if __name__ == "__main__":
auth = AuthContext("admin-1", frozenset({"admin"}), frozenset({"u-1"}))
action = Action(
"intent-001",
"update_profile_status",
{"user_id": "u-1", "status": "suspended"},
)
record_human_approval(auth, action)
print(execute(auth, action))
print(execute(auth, action)) # 命中同一成功结果,不重复副作用6.2 生产差距
- 示例的认证上下文是测试夹具;生产中必须由认证中间件从已验证令牌和授权服务构造,绝不能接受模型或请求体自报角色;
- 示例使用进程内锁和字典;生产中要先用数据库唯一约束原子插入
RUNNING占位,再提交事务,不能持有内存锁或数据库锁跨越慢外部调用; call_id应由服务端根据业务意图签发,并与资源版本或业务唯一键共同防止攻击者换一个 ID 重复同一动作;- 命中幂等结果前仍要鉴权,并校验调用者、动作和规范化参数指纹完全一致;
- 参数需要 JSON Schema/Pydantic 校验;
- 外部供应商优先使用原生幂等 Token;若提交结果不确定,保存
SUBMISSION_UNKNOWN,按供应商任务 ID/客户端键对账,禁止盲目重提; record_human_approval只是测试夹具,生产批准库只能由可信人审接口写入;批准应持久化并绑定call_id、用户身份、完整动作指纹、策略/资源版本、有效期和签名,并在创建执行占位时原子消费;- 不同
call_id并发修改同一资源仍需乐观锁、条件更新或业务串行化; - 审计日志与普通调试日志分开保存。
6.3 必测场景
- 重复请求和重复回调;
- 服务在外部成功、本地落库前崩溃;
- 批准后参数被替换;
- 只读用户请求写工具;
- 外部检索内容包含“忽略系统指令”;
- Trace 关闭敏感输入后仍能定位错误;
- 达到步数、时间或费用上限时可靠终止。
7. 实际项目案例
7.1 示例项目:AI 面试教练工程化
Agent 会读取候选人回答、检索知识、评分并生成追问。虽然大部分工具只读,仍存在评分漂移、记忆污染、提示注入和无限追问风险。
7.2 关键设计
- 题库读取、知识检索为只读工具;
- 评分采用固定量表和结构化输出,不直接把模型分数写为最终事实;
- 用户回答与检索文本标记来源,不能覆盖系统评分规则;
- 每轮保存主题、问题指纹、证据、评分维度和终止原因;
- 最大轮次、单主题预算和退出指令由运行时控制;
- 长期薄弱点记忆需要用户范围隔离、TTL 和删除入口。
7.3 技术难点
该项目仍是设计方案。下列内容用于说明工程难点和验证方法,不代表已经发生过真实事故,也不声明没有实测证据的效果提升。
| 技术难点 | 为什么难 | 关键约束 | 验证方式 |
|---|---|---|---|
| 把概率决策限制在确定性运行时内 | 模型输出会漂移,不能依赖它始终遵守权限、预算和终止条件 | 模型只提出结构化动作;Schema、策略、鉴权、预算和状态迁移由代码执行 | 对同一意图生成多种合法/非法动作,确认非法参数和越权动作始终被运行时拒绝 |
| 至少一次执行下避免重复副作用 | 网络超时、Worker 重启和重复回调会让“是否执行成功”变得不确定 | 幂等键表达业务意图;执行状态原子持久化;结果不确定时先对账 | 注入重复请求、响应丢失和进程崩溃,确认外部业务结果至多产生一次 |
| 人工批准与最终动作保持一致 | 用户批准后模型可能改写对象、金额或其他关键参数 | 批准绑定用户、动作指纹、策略/资源版本和有效期;参数变化必须重新批准 | 批准后篡改任一关键字段,确认执行器拒绝旧批准 |
| 长任务恢复时保持状态一致 | 上下文不是事务日志,重启后可能遗失步骤、重复调用或越过确认 | 持久化状态机、事件历史、检查点、重试预算和幂等记录 | 在模型前后、工具提交前后和落库前后注入崩溃,逐点验证恢复路径 |
| 可观测性与隐私同时成立 | 完整 Trace 有利排障,却可能保存用户回答、检索内容或工具秘密 | 内容最小化、分级脱敏、受控 Artifact、审计访问和保留期限 | 关闭敏感原文后仍可凭 run_id、版本、状态和错误码定位故障;越权读取审计用例被拒绝 |
7.4 方案亮点
| 方案亮点 | 要解决的问题 | 关键决策 | 验证证据或方式 | 适用边界 |
|---|---|---|---|---|
| “模型建议、运行时裁决”双层架构 | Prompt 约束无法成为真实安全边界 | 模型只产生候选动作,服务端重新做允许列表、资源级鉴权、预算和业务状态校验 | 越权参数、伪造身份、未批准动作和策略升级后的旧动作均被执行器拒绝并留下审计事件 | 运行时策略必须独立于模型上下文;只读低风险操作也不能跳过基础鉴权 |
| 业务意图幂等与不确定状态对账 | 工具超时后盲目重试可能重复制造副作用 | 服务端签发意图 ID,原子占位;超时进入 SUBMISSION_UNKNOWN,查询外部真实状态后再推进 | 重复请求返回同一结果;响应丢失测试中先对账而非再次提交 | 外部系统最好支持原生幂等键或按业务键查询;无法对账的不可逆动作需要更强人工控制 |
| 动作指纹绑定人工确认 | 模糊确认无法证明用户批准了最终参数 | 确认展示完整影响并绑定规范化动作指纹、用户、有效期和版本 | 参数替换、确认过期、跨用户复用和重复消费测试全部失败关闭 | 高频低风险动作可按策略降低确认频次,但风险分级必须由确定性规则管理 |
| 事件化状态与可恢复检查点 | 进程内对话状态无法支撑长任务恢复与审计 | 持久化每次状态迁移、工具结果、预算、终止原因和版本,恢复时从最近安全点继续 | 崩溃注入后能区分已完成、待对账和可重试步骤,不重复越过人工确认 | 需要数据库并发控制、事件顺序和 Schema 演进;不能把事件日志误当无限期原文存档 |
| 外部内容与指令隔离 | 检索文档或网页可能通过间接注入操纵工具调用 | 外部内容标记为不可信数据;工具权限、参数与输出由服务端策略校验 | 恶意文档、混淆指令和越权资源测试无法触发高风险工具或读取未授权数据 | 内容检测只能降低风险,最终边界仍是最小权限和人工批准 |
7.5 生产问题闭环
下表是生产风险演练与排障模板。每项都按“现象 → 影响 → 证据 → 根因 → 止损 → 修复 → 验证 → 防复发”闭环记录,不应包装成真实项目事故。
| 现象 | 影响 | 定位证据 | 可能根因 | 止损 | 修复 | 验证 | 防复发 |
|---|---|---|---|---|---|---|---|
| 工具返回超时,但外部动作可能已成功 | 直接重试可能重复扣费、发消息或修改资源 | run_id、call_id、动作指纹、外部任务 ID、提交时间和网络错误 | 响应丢失;本地在外部成功后未落库 | 将状态置为 SUBMISSION_UNKNOWN,暂停自动重提并限制同资源后续写入 | 按幂等键或外部任务 ID 对账,再原子推进为成功、失败或可重试 | 注入“外部成功、响应丢失”,确认外部结果仅出现一次且本地最终收敛 | 所有副作用工具提供业务幂等键、对账接口、不确定状态告警和恢复演练 |
| Webhook 重复或乱序到达 | 重复推进、状态回退或补偿被多次执行 | event_id、业务版本、到达顺序、处理记录和状态变更日志 | 至少一次投递;缺唯一约束和条件更新 | 暂停冲突资源消费,将异常事件隔离到待审队列 | event_id 去重,按资源版本条件更新;乱序事件重放或忽略 | 重复、乱序和并发回调测试后业务状态唯一且单调合法 | 唯一约束、状态机合法迁移、死信队列和冲突率告警 |
| 用户读取到其他用户的长期记忆 | 隐私泄露并污染后续决策 | 认证主体、tenant/user scope、查询条件、缓存键、返回记录和审计日志 | 作用域过滤缺失;缓存未隔离;模型提供了不可信 user_id | 关闭相关记忆读取/缓存,撤销访问并启动安全审计 | 服务端注入主体范围,执行行级授权,缓存绑定权限范围并支持删除 | 跨用户、同租户不同权限、撤权和删除回归全部通过 | 权限测试进入发布门禁;异常跨主体命中告警;定期审计数据保留与删除 |
| 检索文档诱导 Agent 调用高风险工具 | 越权操作、敏感数据泄露或错误业务动作 | 文档来源、模型决策、候选动作、Guardrail/策略命中和工具审计 | 外部内容被当成系统指令;服务端工具策略过宽 | 禁用受影响高风险工具或强制人工批准,隔离恶意来源 | 内容与指令分层,收紧允许列表和资源权限,关键参数由可信数据源填写 | 直接/间接注入语料测试无法绕过服务端策略 | 恶意样本回归、最小权限审查、来源信誉和高风险调用告警 |
| Agent 持续循环同类工具或追问 | 请求长时间不结束,Token、费用和队列占用增长 | 步数、工具指纹、状态变化、预算、无进展次数和终止原因 | 缺最大步数/时间预算;无重复动作与无进展检测 | 取消当前 Run、限流相关入口并释放占用资源 | 增加总预算、动作去重、进展判定和明确终止状态 | 长对话、同参工具循环和依赖持续失败测试能在预算内终止 | 循环步数、无进展次数、预算耗尽率告警和回归样本 |
| 进程重启后状态丢失或重复执行已完成步骤 | 任务卡住、结果丢失或副作用重复 | 状态表、事件历史、检查点、幂等记录、Worker 日志和外部结果 | 状态只在内存;状态与工具结果非原子记录;恢复逻辑缺失 | 暂停自动恢复,对待定步骤逐一对账 | 持久化状态机和检查点,按幂等记录恢复,跨系统结果使用对账或补偿 | 在关键边界注入崩溃,重启后任务收敛且不越过确认 | 定期恢复演练、卡住状态告警、状态迁移不变量和补偿测试 |
| 同一答案评分波动或持续追问同一角度 | 反馈失真,面试流程难以稳定验收 | rubric/prompt/model 版本、采样配置、问题指纹和覆盖矩阵 | 量表模糊、随机性高、问题目标与去重缺失 | 暂停写入长期能力结论,回退稳定量表和轮次上限 | 结构化 rubric、示例锚点、低随机性、问题指纹和覆盖约束 | 固定样本重复评测与长对话回归,检查分歧和主题覆盖 | 评分版本化、漂移抽检、重复问题率和异常轮次告警 |
7.6 可复用经验卡
| 经验卡 | 核心规则 | 固化动作 | 验收证据 | 适用边界 |
|---|---|---|---|---|
| 信任边界落在工具服务端 | 模型可以建议,不能授权自己 | 认证上下文由中间件注入;工具前重新鉴权、校验参数和业务状态 | 伪造身份或 Prompt 注入后仍无法访问未授权资源 | Guardrail 是补充,不替代 IAM、行级权限和业务校验 |
| 超时不等于失败 | 未收到响应不代表外部未执行 | 显式 SUBMISSION_UNKNOWN、停止盲重试、按外部标识对账 | 响应丢失和恢复测试中不产生重复副作用 | 只读且真正幂等的调用可按受控策略直接重试 |
| 幂等键代表业务意图 | 网络请求 ID 无法阻止换 ID 重复同一动作 | 用用户、资源、动作、规范化参数和意图版本构造唯一意图 | 重复请求、并发请求和重启后重放返回同一业务结果 | 随机生成任务需要区分“重试同一意图”和“用户明确重新生成” |
| 参数变化就使旧批准失效 | 人工批准的是具体影响,不是无限授权 | 批准绑定动作指纹、身份、资源/策略版本和有效期,执行时原子消费 | 任一关键参数变化、过期或跨用户复用都被拒绝 | 批量批准需要明确范围、上限和可审计的展开规则 |
| 每种不确定性都进入显式状态 | 模糊异常会诱发错误自动恢复 | 为待批准、执行中、结果未知、待补偿和终止原因定义状态与合法迁移 | Trace 和状态表能解释每次恢复决策,不存在静默跳步 | 状态数量要服务于恢复和审计,避免无业务意义的过度细分 |
| 可观测与隐私一起设计 | 事后补日志常导致“不可排障”或“过度采集” | 先定义诊断问题,再选择脱敏字段、Artifact 权限、采样与保留期 | 不保存秘密也能定位版本、步骤和错误;敏感内容访问可审计 | 高风险审计可能要求更长保留期,应由合规和业务共同确定 |
8. 方案权衡与常见误区
8.1 Guardrail 的时机
- 输入 Guardrail:适合阻断明显违规请求,但不能理解所有外部数据影响;
- 工具 Guardrail:靠近副作用,适合参数和结果检查;
- 输出 Guardrail:检查最终回答,但不能撤销已经执行的工具副作用;
- 服务端权限:独立于模型,是最终授权边界。
8.2 常见误区
- 只在 System Prompt 中写“不要泄露秘密”;
- 所有错误都自动重试;
- 把模型生成的用户 ID、金额或资源 ID 当可信参数;
- 记录完整 Trace,却没有敏感数据控制;
- 用内存对话保存长任务状态;
- 认为人工确认后后续所有参数都自动获得授权。
8.3 成本与延迟权衡
- 每步评审模型会增加延迟,可只对高风险动作启用;
- 全量 Trace 有利排障,但需采样、脱敏和保留期限;
- 多 Agent 隔离职责会增加调用和上下文成本;
- 严格去重与缓存提高稳定性,但随机创作任务需要保留“重新生成”语义。
9. 面试题与参考答案
9.1 为什么 Agent 工具调用必须幂等?
- 合格答案:网络、Worker 和回调通常是至少一次执行,重试可能重复副作用;
- 加分项:说明业务意图幂等键、唯一约束、外部任务 ID 和对账;
- 常见错误:认为 HTTP 重试库能自动保证幂等。
9.2 Prompt Injection 和越权有什么区别?
- 合格答案:注入试图操纵模型决策,越权是执行系统未正确限制资源或动作;
- 加分项:说明即使模型被注入,最小权限仍应阻止真实危害;
- 常见错误:只通过关键词过滤解决两者。
9.3 工具超时后为什么不能直接重试?
- 合格答案:外部可能已成功,只是响应丢失;直接重试会重复副作用;
- 加分项:使用幂等键查询或对账,再决定重试;
- 常见错误:把超时等同于未执行。
9.4 如何设计人工确认?
- 合格答案:展示动作、对象、关键参数、影响和可撤销性,批准绑定动作指纹与有效期;
- 加分项:参数改变重新确认,确认事件可审计;
- 常见错误:只弹一个“是否继续”而不展示内容。
9.5 Agent 可观测性至少记录什么?
- 合格答案:模型调用、工具、Guardrail、Handoff、状态、错误、Token、成本和最终结果;
- 加分项:敏感数据开关、高基数处理和回归评测关联;
- 常见错误:只记录最终回答。
10. 递进追问
- 基础概念:重试、幂等、补偿和对账分别解决什么问题?
- 原理细节:为什么“模型先判断是否有权限”不是安全边界?
- 实现边界:如何处理同一 Webhook 的重复和乱序到达?
- 工程权衡:全量保存模型输入输出有哪些隐私和成本问题?
- 系统设计:如何给一百个租户隔离工具权限、预算和长期记忆?
- 项目复盘:如果 Agent 误删数据,应该如何从事件、审批和工具日志定位责任链?
11. 实践任务
- [ ] 为只读、可逆写入和不可逆写入设计三级工具策略;
- [ ] 使用数据库唯一约束替换示例内存幂等表;
- [ ] 模拟“外部成功、内部超时”,实现查询对账;
- [ ] 加入动作指纹绑定的人工确认;
- [ ] 设计一组直接和间接 Prompt Injection 测试;
- [ ] 输出一次 Agent Run 的 Trace、Metric、Log 字段清单。
- [ ] 将一次故障注入按“现象 → 影响 → 证据 → 根因 → 止损 → 修复 → 验证 → 防复发”整理成复盘卡。
12. 相关知识与参考资料
12.1 相关知识
- 前置:Agent 核心机制
- 架构:AI 应用系统设计
- 项目:AI 面试教练项目
- 长任务案例:AI 视频生产工作流项目
12.2 一手参考资料
以下资料于 2026-07-10 核对:
- OpenAI Agents SDK - Guardrails
- OpenAI Agents SDK - Tracing
- OpenAI Agents SDK - Running Agents
- OWASP Top 10 for LLM Applications 2025
- OpenTelemetry Signals
- Temporal Documentation
13. 简明总结
一句话记忆: 生产 Agent 要默认模型、网络和外部系统都会出错,再用确定性运行时限制损失。
- 权限、预算、幂等和终止条件必须由代码掌握;
- 超时不代表未执行,不确定结果应先对账;
- Guardrail 辅助模型安全,服务端最小权限才是最终边界;
- 状态持久化和全链路观测决定系统能否恢复与排障;
- 每个高风险动作都应可解释、可确认、可审计,生产问题必须以证据、验证和防复发闭环。