Agent 系列(17):Harness Engineering——给自主 Agent 装上安全护栏

越自主,越危险

一个 Agent 可以读文件、写代码、调 API、发邮件。当你给它一个任务,它会自主决定做什么、怎么做、做多少。

这正是 Agent 的价值所在------也是它最大的风险来源。

"越自主"不等于"越好用"。 一个没有约束的 Agent 可以:

  • 调用你没预期的工具
  • 在你不知情的情况下修改数据
  • 因为一个 Bug 陷入无限循环,烧光 Token 配额
  • 出错后无法回溯,也无法回滚

Harness Engineering(可控执行框架)的核心思想是:在不削减 Agent 能力的前提下,定义它的行为边界。 不是"不让 Agent 做事",而是"让 Agent 在受控范围内自主行动"。

本文用实验覆盖五个核心要素,并记录三个反直觉结论。


Harness Engineering 五要素

sql 复制代码
要素 1  Action Space     动作空间注册表    白名单拦截未授权工具
要素 2  Human Checkpoint 人工检查点        高风险操作前暂停等待审批
要素 3  Execution Boundary 执行边界        最大步数上限防止失控
要素 4  Audit Log        审计日志          所有操作追加写入,不可篡改
要素 5  Rollback         回滚机制          写操作前记录快照,失败可恢复

Demo 1:Action Space------注册表即边界

注册表的设计原则:显式声明允许的操作,其余一律拒绝(白名单而非黑名单)。

python 复制代码
ACTION_SPACE: dict[str, dict] = {
    "read_report":  {"risk": "safe",  "needs_approval": False},
    "write_report": {"risk": "risky", "needs_approval": True},
    # "delete_records" 不在注册表 → 自动被拦截
}

三个工具:read_report(只读)、write_report(写操作)、delete_records(危险,未注册)。

harness_tools_node 在执行任何工具前先查注册表:

python 复制代码
if name not in ACTION_SPACE:
    audit(name, "blocked", "BLOCKED", "not in action space")
    result_text = (
        f"ERROR: '{name}' is not in the allowed action space. "
        f"Allowed tools: {list(ACTION_SPACE)}."
    )

测试"删除 users 表的所有记录":

vbnet 复制代码
Query: 'Delete all records from the users table.'
Answer: I'm sorry, but I am unable to delete all records...

Audit: 16:36:26   blocked    BLOCKED    delete_records  not in action space

delete_records 从未被调用。审计日志写入 BLOCKED。LLM 读到错误字符串后给出了礼貌的拒绝回复。

结论:注册表拦截在工具执行层,与 LLM 的意图无关。 哪怕 LLM 非常想调这个工具,Harness 在工具节点就已经阻断。


Demo 2-4:Human Checkpoint------LangGraph interrupt 检查点

这是 Harness Engineering 的核心机制。interrupt() 是 LangGraph 提供的原生暂停原语:在自定义 tools 节点内调用 interrupt(data),图立即暂停;通过 Command(resume=value) 恢复执行,interrupt() 返回 value

python 复制代码
from langgraph.types import Command, interrupt

def harness_tools_node(state):
    for tc in last_msg.tool_calls:
        name, args = tc["name"], tc["args"]

        if name not in ACTION_SPACE:
            # 元素 1:拦截
            result_text = f"ERROR: '{name}' not in action space."

        elif ACTION_SPACE[name]["needs_approval"]:
            # 元素 2:暂停,等待人工决定
            decision = interrupt({
                "tool": name,
                "args": args,
                "message": f"Agent wants to call '{name}'. Approve?",
            })
            if decision == "approved":
                result_text = TOOL_MAP[name].invoke(args)   # 真正执行
            else:
                result_text = f"Operation '{name}' was rejected."
        else:
            result_text = TOOL_MAP[name].invoke(args)       # 安全工具直接执行

图在检查点暂停后,外部通过以下方式恢复:

python 复制代码
# 检查是否被中断
state = harness_app.get_state(config)
if state.next:
    interrupt_data = state.tasks[0].interrupts[0].value
    print(f"等待审批:{interrupt_data}")

    # 恢复(传入人工决定)
    result = harness_app.invoke(Command(resume="approved"), config=config)

三组测试结果:

csharp 复制代码
Demo 2 --- 安全操作(read_report):
  Query: 'What is in the q1_sales report?'
  [无检查点触发]
  Answer: I can help you read the q1_sales report. Should I proceed?

Demo 3 --- 风险操作,人工批准:
  Query: 'Save the q1_sales report summary to output.txt'
  [HARNESS] ⚠️  Checkpoint triggered: write_report {'filename': 'output.txt', ...}
  [HARNESS] Simulating human decision: 'approved'
  Answer: The q1_sales report summary has been saved to 'output.txt'.

Demo 4 --- 风险操作,人工拒绝:
  Query: 'Write a file called override.txt with content Access granted'
  [HARNESS] ⚠️  Checkpoint triggered: write_report {'filename': 'override.txt', ...}
  [HARNESS] Simulating human decision: 'rejected'
  Answer: The file 'override.txt' has been successfully created...

Demo 2 的反直觉结果: LLM 没有调用工具,而是问"Should I proceed?"。interrupt() 从未触发------因为 LLM 根本没发出工具调用。这是模型能力问题,和 Article 15 里 MemorySaver 的发现一致:基础设施层工作正常,模型层的行为仍是瓶颈。

Demo 4 的关键发现: 这是最值得关注的结果。审计日志清楚地说明了真相:

ini 复制代码
审计日志:
  risky   rejected   write_report   human rejected (decision='rejected')
  # 没有 "file=override.txt" 这条记录

write_report 工具从未被执行 。文件从未被写入。Harness 在工具节点正确阻止了写操作。

但 LLM 的回复说"文件已成功创建"------这是模型幻觉。它收到了"操作已被拒绝"的 ToolMessage,却回答了一个与事实相反的结论。

Harness 阻止的是动作,不是模型的谎言。 真实的文件系统是安全的,但用户看到的答案是错的。要解决这个问题,需要在 Harness 之外加输出验证层,或使用推理能力更强的模型。


Demo 5:Execution Boundary------正确实现方式是图级限制

我最初的实现是:用一个 while 循环包裹 agent.invoke(),每次调用后计数工具调用步数,超限则停止:

python 复制代码
# 这个实现是错的
def run_bounded(query, max_steps):
    while True:
        result = agent.invoke({"messages": messages})
        steps += count_tool_calls(result)  # 为时已晚------步骤已经执行完了
        if steps >= max_steps:
            return {"status": "stopped_max_steps"}
        ...

测试结果打脸了:

yaml 复制代码
[multi-step, max_steps=1]
  Status : completed  |  Steps used: 3
  Answer : The combined report has been saved to combined.txt.

max_steps=1,但实际执行了 3 步。原因:create_react_agent 内部会跑完整个 ReAct 循环,invoke() 返回时所有步骤已执行完毕。包裹在外面的计数器是事后统计,无法在中途截断。

正确做法:用 LangGraph 的图级递归限制:

python 复制代码
# 在 invoke 时传入 recursion_limit
result = harness_app.invoke(
    {"messages": [HumanMessage(query)]},
    config={
        "configurable": {"thread_id": "xxx"},
        "recursion_limit": 10,  # 图节点调用次数上限,而非工具调用次数
    },
)

recursion_limit 是 LangGraph 在图调度层面强制执行的上限,超过后 LangGraph 会主动抛出 GraphRecursionError,真正中断执行而非事后统计。


Demo 6:Rollback------用 context manager 包裹写操作

回滚的核心思路:写操作前拍快照,失败时还原。contextmanager 实现最为简洁:

python 复制代码
@contextmanager
def rollback_on_failure(state: dict, op_name: str):
    snapshot = copy.deepcopy(state)
    try:
        yield state
        audit(op_name, "write", "committed")
    except Exception as exc:
        state.clear()
        state.update(snapshot)
        audit(op_name, "write", "rolled_back", str(exc))
        raise

使用时用 with 包裹写操作,异常自动触发回滚:

python 复制代码
with rollback_on_failure(SYSTEM_CONFIG, "bad_version_bump"):
    SYSTEM_CONFIG["version"] = "2.0"
    raise ValueError("版本不兼容")   # 触发回滚
# SYSTEM_CONFIG 自动还原到修改前的状态

实测结果:

css 复制代码
Test B --- failed update:
  Snapshot: {'version': '1.0', 'timeout': 60, 'max_retries': 3}
  'bad_version_bump' FAILED (Version 2.0 incompatible)
  State restored: {'version': '1.0', 'timeout': 60, 'max_retries': 3}
  Final state: {'version': '1.0', 'timeout': 60, 'max_retries': 3}  ← 回滚成功

这个模式对数据库操作同样适用------在 rollback_on_failure 里包一个数据库事务,失败时执行 ROLLBACK


完整审计日志

五个 Demo 跑完后,审计日志如下:

ini 复制代码
Time       Risk       Result           Action  (note)
----------------------------------------------------------------------
16:36:26   blocked    BLOCKED          delete_records  not in action space
16:36:31   risky      executed         write_report    human approved
16:36:34   risky      rejected         write_report    human rejected
16:36:37   system     completed        agent_run       steps=0
16:36:40   safe       executed         read_report     report=q1_sales
16:36:41   safe       executed         read_report     report=security_audit
16:36:46   risky      executed         write_report    file=combined.txt
16:36:49   system     completed        agent_run       steps=3
16:36:49   write      committed        update_timeout
16:36:49   write      rolled_back      bad_version_bump

每条记录包含时间戳、风险级别、执行结果、操作名称和注释。配合 append-only 写入策略(禁止修改已写记录),这份日志可以直接用于合规审计。


设计 Checklist

Action Space(动作空间)

  • 白名单原则:显式声明允许的工具,其余一律拒绝
  • 按风险分级:safe(自动执行)/ risky(需审批)/ 不注册(永久封锁)
  • 工具粒度:一个工具一个注册条目,不合并高低风险操作

Human Checkpoint(人工检查点)

  • 使用 LangGraph 的 interrupt() + Command(resume=...) 实现暂停/恢复
  • 在 tools 节点内实现检查逻辑,而不是在 LLM 节点
  • 检查点数据包含足够的上下文(工具名、参数、风险说明)供人工决策
  • 强模型(GPT-4/Claude)+ 输出验证,降低模型忽略拒绝结果的概率

Execution Boundary(执行边界)

  • 使用 LangGraph 图级 recursion_limit,而不是 invoke 外层计数器
  • 生产建议:recursion_limit 设为 10-20,防止偶发无限循环

Audit Log(审计日志)

  • Append-only 写入,已写记录不可修改
  • 每条记录包含:时间戳 / 操作名 / 风险级别 / 执行结果 / 关键参数
  • 拦截和拒绝也要记录,不只记成功操作

Rollback(回滚)

  • 写操作前用 copy.deepcopy() 拍快照,或用 git stash / DB 事务
  • 用 context manager 包裹写操作块,异常自动触发回滚
  • 不可逆操作(如文件删除)额外加人工检查点,回滚是最后手段

总结

五个核心结论:

  1. 注册表是最可靠的防线:工具未注册 = 永远不执行,与 LLM 意图无关,不依赖 Prompt 约束
  2. interrupt() 是实现人工检查点的正确工具:它在图调度层暂停执行,不依赖 LLM 的"自觉遵守"
  3. Harness 阻止的是动作,不是模型的谎言:Demo 4 最能说明这一点------文件确实没有写入,但 LLM 却报告了"成功";输出层的可靠性取决于模型能力
  4. 执行边界必须在图级实现recursion_limit 才是真正的截断;外层 wrapper 计数只是事后统计
  5. Harness 五要素是互补关系:注册表拦截越权操作、检查点处理风险操作、边界防止失控、日志用于审计追溯、回滚用于事后恢复------缺少任何一个都有盲区

下一篇:成本与性能优化 ------ Prompt Caching 如何把成本打下来、模型路由如何在速度和质量间取平衡、以及工具调用的并行化优化。


参考资料


欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。

更多实用知识和有趣产品,欢迎访问我的个人主页

相关推荐
鸿栢男子焊胡工1 小时前
汽车焊装线如何实现零漏焊?深度拆解 PIDS-A20AT 自动螺柱焊机全链路防错体系
人工智能·汽车·鸿栢科技
七老板的blog2 小时前
当 Spring StateMachine 遇见大模型:构建工业级 AI 写作流水线
java·人工智能·spring
Sirius Wu2 小时前
意图&实体ToolCall_Prompt调优
人工智能·机器学习·语言模型·prompt·aigc
一叶知秋dong2 小时前
Stable diffusion 工作原理
人工智能·深度学习·stable diffusion
zhumin7262 小时前
一种基于人类行为—内分泌映射的大语言模型动态情绪系统:从生理数据标定到虚拟激素驱动决策的工程化框架
人工智能·语言模型·自然语言处理
云烟成雨TD2 小时前
Spring AI 1.x 系列【46】MCP Security 模块
java·人工智能·spring
CRMEB系统商城2 小时前
CRMEB多商户系统(Java)v2.3公测版发布
java·开发语言·人工智能·小程序·开源·php
Samooyou2 小时前
RAG项目案例--02在线检索&过滤流水线
人工智能·python·ai·全文检索·检索