04 Agent 的确定性边界:让 LLM 规划,让代码验证和执行
结论先说:Agent 的核心边界不是“能不能调用工具”,而是“谁有最终执行权” #
看 SecurityClaw 的执行链,最重要的设计不是 LLM 会不会规划,而是:LLM 只能提议动作,不能直接拥有执行权。
这篇要讲清三件事:
- 为什么 Agent 不能让 LLM 直接输出 SQL、Shell 或真实 API 请求
- 运行时验证层到底在拦什么
- 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"
这个测试比最终生成的一段文案更可靠,因为它证明了:
- 模型给错字段时系统不会静默执行
- 错误会被结构化返回
- 行为可重复复现
换句话说:
文案只能说明“系统看起来像对了”,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 在运行时被强约束
- 错误路径和恢复路径可测试