1. SecurityClaw 学习笔记/

04 Agent 的确定性边界:让 LLM 规划,让代码验证和执行

结论先说:Agent 的核心边界不是“能不能调用工具”,而是“谁有最终执行权” #

看 SecurityClaw 的执行链,最重要的设计不是 LLM 会不会规划,而是:LLM 只能提议动作,不能直接拥有执行权

这篇要讲清三件事:

  1. 为什么 Agent 不能让 LLM 直接输出 SQL、Shell 或真实 API 请求
  2. 运行时验证层到底在拦什么
  3. Mock 和测试为什么比一段漂亮的最终文案更可靠

一、先把边界画出来:LLM 只生成“计划对象” #

如果你把 Agent 粗暴地理解成“让大模型去调工具”,那一定会写出这种危险代码:

# 危险示例:让模型直接输出可执行内容
sql = llm("根据用户问题直接输出 SQL")
rows = db.execute(sql)

这类设计的问题不是“不优雅”,而是执行边界彻底丢了

  • 模型可能引用不存在的表或字段
  • 测试环境和生产环境 schema 不一致
  • 模型可能把查询写成更新、删除
  • 一旦失败,你很难判断是模型理解错、schema 变了,还是权限不足

SecurityClaw 走的是另一条路:

# 伪代码:LLM 只生成结构化计划
class ToolPlan(TypedDict):
    action: str
    tool_name: str
    arguments: dict
    expected_output: str


def plan_with_llm(question: str, context: str) -> ToolPlan:
    # LLM 只负责把用户意图翻译成结构化动作
    return llm.generate_structured_plan(question, context)

也就是说,LLM 产出的不是“最终可执行命令”,而是一个待验证的计划对象


二、运行时验证层到底做什么 #

真正的控制权在运行时。抽象成伪代码,大概是这个顺序:

# 伪代码:运行时验证与执行
class RuntimeExecutor:
    def run(self, plan: ToolPlan, schema: SchemaInfo, permissions: PermissionSet):
        self._validate_plan_fields(plan, schema)
        self._check_allowed_tools(plan, permissions)
        self._check_argument_shape(plan)
        self._check_budget(plan)
        return self._dispatch(plan)

你可以把这层理解成 Agent 的“法官”,不是“助手”。

2.1 字段验证 #

最基础的一步就是确认计划引用的字段或参数真的存在:

if not self._validate_plan_fields(plan, self.schema):
    return PlanValidationError("schema mismatch")

这一步解决的是模型幻觉和环境漂移

  • 模型猜了一个不存在的字段
  • 测试库和生产库字段名不一致
  • 工具 schema 升级后,旧 prompt 还在用老字段

2.2 权限验证 #

Agent 不是“会调用工具的 LLM”,而是“在权限边界内规划的系统”。

if not self.authorizer.check(plan["tool_name"], user_context):
    return PermissionDenied("tool not allowed")

这个判断的位置非常关键:权限必须在执行前检查,而不是让模型“自己记得别乱来”

2.3 预算验证 #

Agent 一旦进入循环,很容易把 token、API 调用次数、外部查询成本一起烧光。

if self.budget.remaining < self.cost_table[plan["tool_name"]]:
    return BudgetExceeded("over budget")

这一步的价值不是省钱这么简单,而是把“是否值得继续”从模型情绪里拿出来,变成一个可预测的系统条件。


三、为什么不直接让 LLM 输出 SQL 或 DSL #

这个 tradeoff 很值得单独讲。

方案 A:让 LLM 直接输出最终执行内容 #

优点:

  • 实现快
  • demo 好看
  • prompt 写好就能跑

缺点:

  • 没有执行边界
  • 很难做 schema 漂移保护
  • 失败不可诊断
  • 很难做权限和预算隔离

方案 B:让 LLM 输出结构化计划,代码去解释执行 #

优点:

  • 验证层可插拔
  • schema 和权限可独立演进
  • 测试可覆盖
  • 失败可回溯

缺点:

  • 需要多写一层 schema 和运行时模板
  • 需要维护 plan 的结构
  • 初期开发比直接 prompt 多一点工程量

坦率地讲,如果你只是做本地 demo,方案 A 更快。但只要是长期运行、多人协作、或者安全场景,方案 B 基本是唯一靠谱路线。


四、Mock 为什么比最终回答更可靠 #

技术博客里最容易犯的错,是只展示“最后给用户看到的答案”,不展示“系统是怎么证明这个答案可靠的”。

SecurityClaw 的一个强点,就是大量逻辑都能被 Mock 隔离测试。

@pytest.fixture
def mock_skill():
    provider = MockDataProvider(schema={"country_code", "year", "sales_amount"})
    return SkillExecutor(provider)


def test_invalid_field_rejected(mock_skill):
    plan = {
        "tool_name": "query_sales",
        "arguments": {"field": "revenue_total"}  # 不存在字段
    }
    result = mock_skill.validate_and_run(plan)
    assert result.status == "schema_mismatch"

这个测试比最终生成的一段文案更可靠,因为它证明了:

  1. 模型给错字段时系统不会静默执行
  2. 错误会被结构化返回
  3. 行为可重复复现

换句话说:

文案只能说明“系统看起来像对了”,Mock 和 trace 才能说明“系统在错误路径也没失控”。


五、设计边界:LLM 应该负责什么,不应该负责什么 #

LLM 适合负责 #

  • 把自然语言问题翻译成结构化目标
  • 在多个候选工具之间做高层选择
  • 基于已有 observation 做再规划
  • 生成最终面向用户的解释文本

LLM 不适合直接负责 #

  • 权限判断
  • schema 真实性判断
  • 预算控制
  • 最终执行
  • 错误恢复策略的硬边界

你可以用一句话记住:

LLM 适合做语义压缩,不适合做系统边界判断。


六、这一层如果写坏了,会怎么失败 #

失败路径 1:模型建议了不存在字段 #

运行时应返回:schema mismatch

失败路径 2:模型选中了没有权限的工具 #

运行时应返回:permission denied

失败路径 3:模型进入重复规划 #

运行时应在预算或最大轮次处强制停止

失败路径 4:工具执行成功但结果无意义 #

运行时应把 observation 回流给评估层,而不是直接拼成答案

这也是为什么我一直强调:Agent 的可靠性来自运行时设计,而不是 prompt 文采。


七、验证命令 #

pytest tests/ -k "schema or permission or runtime" -v

如果你要手动验证“LLM 规划、代码执行”的边界,最应该看的不是最终答案,而是:

pytest tests/test_runtime_validation.py -v
pytest tests/test_permission_guard.py -v

八、带走的一个框架 #

理解 Agent 的确定性边界,可以用这条链记:

用户问题
  -> LLM 生成计划对象
  -> 运行时验证计划对象
  -> 工具执行
  -> observation 回流
  -> LLM 再规划 / 生成最终答案

真正可靠的 Agent,不是“模型很聪明”,而是:

  • 规划和执行分离
  • 执行权收回到代码
  • 权限、预算、schema 在运行时被强约束
  • 错误路径和恢复路径可测试