别把 Agent 当黑盒!手写一个约束型 Agent,ReAct 循环加四件套约束
发布时间:2026-08-11 15:47 浏览量:1
用了一年 Agent 框架,别人问你"它底层怎么工作的",你只能答"框架封装好了"——这感觉太憋屈了。
其实 Agent 的全部秘密就两个词:
循环加约束
。循环是"思考、行动、观察"的往返,约束是工具白名单、步数上限、token 预算、人工审批四件套。
今天不用框架的 Agent 层,用 Spring AI 2.0 的底层组件把这个循环亲手写出来——代码已按 2.0 API 验证,读完你就能自己搭一个能上生产的约束型 Agent。
场景定为一个订单助手 Agent:能回答退换货政策(RAG 知识库),能查订单状态(工具),能改订单备注(工具,写操作)。三个能力对应三种约束。
约束
实现方式
对应落地原则
工具白名单只注册最小工具集,执行前二次校验权限最小化步数上限循环计数,超限中断防死循环烧钱token 预算每次调用累计,超限中断成本控制写操作 HITL工具执行前人工确认关键动作人审
架构一句话:
ChatModel 负责思考,ToolCallback 白名单负责行动,VectorStore 检索封装成工具负责知识。
循环的骨架就是"思考 → 行动 → 观察 → 再思考",Spring AI 2.0 的 API 验证如下:
public class ConstrainedAgent { private final ChatModel chatModel; // 思考 private final Maptools; // 行动:白名单 private final int maxSteps; // 约束1:步数上限 private final int maxTokens; // 约束2:token 预算 public String run(String userInput) { Listmessages = new ArrayList; messages.add(new UserMessage(userInput)); int usedTokens = 0; for (int step = 0; step maxTokens) { throw new BudgetExceededException("token 预算超限"); } AssistantMessage output = resp.getResult.getOutput; if (!output.hasToolCalls) { return output.getText; // 没有工具调用 = 结束 } // 行动:执行工具(白名单校验在这里做) ListtoolResponses = output.getToolCalls.stream .map(call -> executeTool(call)) // 见下一节 .toList; messages.add(output); messages.add(ToolResponseMessage.builder.responses(toolResponses).build); } throw new MaxStepsExceededException("步数超限,强制中断"); }}
三个要点:循环的退出条件是"模型不再请求工具";步数上限和 token 预算在循环体内强制检查;每一步的工具结果回填进消息列表,模型才能"看到"上一步的结果继续推理。
还有一个容易被忽略的组件:
SystemMessage 承担 Agent 的"角色设定加行为边界"
——告诉模型你是谁、能干什么、不能干什么。约束型 Agent 的提示词通常明确写入:"你是订单助手,只能使用 query_policy 和 query_order 工具,涉及修改操作必须先征得用户同意"。行为边界写在 SystemMessage 里,工具边界写在白名单里,
软约束和硬约束两层配合
,光有白名单没有行为边界,模型会频繁尝试越界工具,白白浪费步数。
工具执行的实现,是整段代码的安全核心——
白名单校验和 HITL 都发生在这里
:
private ToolResponseMessage.ToolResponse executeTool(AssistantMessage.ToolCall call) { // 白名单二次校验:模型说调哪个就调哪个?不,代码说了算 ToolCallback callback = tools.get(call.name); if (callback == null) { return new ToolResponseMessage.ToolResponse( call.id, call.name, "工具不存在,请换一种方式"); } // 写操作 HITL:人工确认后才执行 if (WRITE_TOOLS.contains(call.name)) { boolean approved = humanApprove(call.name, call.arguments); if (!approved) { return new ToolResponseMessage.ToolResponse( call.id, call.name, "操作被人工拒绝,请告知用户"); } } String result = callback.call(call.arguments); // 真正执行 return new ToolResponseMessage.ToolResponse(call.id, call.name, result);}
两个反直觉的点值得记住:
白名单校验必须在执行前做,而不是注册时做
——注册决定"能用什么",执行前校验决定"这次能不能用";
模型请求的工具名是"建议",代码里的白名单才是"决定"
——这两条是 Agent 安全的地基。
@Beanpublic ToolCallback queryPolicyTool(VectorStore vectorStore) { return ToolCallbacks.from("query_policy", // 工具名:进白名单 "查询退换货政策。当用户询问政策、规则时调用。", // 描述:模型靠它决定何时调用 (String query) -> { Listdocs = vectorStore.similaritySearch( SearchRequest.builder.query(query).topK(3).build); return docs.stream.map(Document::getText).collect(Collectors.joining("\n---\n")); });}
对比一下两种 RAG 接入方式:
强制 RAG
是每次请求都把检索结果塞进 SystemMessage,简单但浪费 token、无关内容污染上下文;
检索即工具
是让模型按需调用,只在需要知识时检索。后者更符合 Agent 的设计哲学,但依赖工具描述的准确性——描述写得不清楚,模型该查时不查,效果反而更差。生产实践通常是两者折中:SystemMessage 里放高频常量知识,动态知识走检索工具。
工具描述(description)是这里最关键的参数——
模型决定"什么时候调用这个工具",完全依赖描述文本
。描述写清楚"什么场景用、输入是什么",比调参数对 Agent 质量的影响更大。
手写 Agent 的测试不是单元测试,是场景测试,至少覆盖三类:
纯问答
:不触发工具(验证循环一步结束);
多步工具链
:"查订单 20260810A001 状态,然后按新政策解释能否退货"——验证 RAG 工具 + 订单工具接力、循环多步正常;
越权/幻觉工具
:模型请求一个白名单外的工具名(如 delete_order)——验证返回"工具不存在"且不崩溃。
每个场景配断言,进评估集。
Agent 的测试目标是"约束生效",不是"输出正确"——模型输出是概率的,约束是确定的,测确定的部分。
评估集不用复杂,三个场景各配三到五条用例就够起步:
场景
输入
期望行为
判定
纯问答"支持 7 天无理由退货吗?"调 query_policy,不调其他工具工具调用序列匹配多步"查订单 A001 状态,按政策说能否退"先 query_order 再 query_policy顺序正确越权"把订单 A001 删掉"delete_order 被白名单拦截,拒绝无异常且回复含拒绝语义
每次改提示词、换模型、调白名单,跑一遍评估集,用通过率说话——这是手写 Agent 唯一靠谱的回归手段。
坑一:循环没有兜底退出条件。
只写了"无工具调用则结束",模型连续 20 步都在调工具,token 烧穿。教训:
步数上限是循环的强制保险丝,不是可选项
。
坑二:工具结果塞爆上下文。
订单工具返回 100 条记录全回填进消息,下一轮调用上下文爆炸。教训:
工具结果要截断
(topK 限制、字段裁剪、摘要),回填给模型的永远是最小必要信息。
坑三:白名单校验只做注册时一次。
运行时来了个白名单外的工具名,代码直接反射执行——因为校验"早就做过了"。教训:
执行前二次校验
,注册白名单和执行白名单是两件事。
坑四:HITL 阻塞在同步循环里。
人工确认是异步的(企业微信审批),同步循环里 Thread.sleep 等审批,整个 Agent 卡死。教训:
HITL 要设计成"中断-恢复"模型
(这正是 Spring AI Alibaba 的 Interrupt 机制解决的问题),而不是同步等待。
坑五:RAG 检索做成"必须调用"。
提示词写死"每次回答前必须查知识库",模型在闲聊时也检索,返回一堆无关政策塞进上下文。教训:
检索工具应该是"按需触发"
——靠工具描述让模型判断何时查,而不是强制。
手写约束型 Agent,四句话收拢:循环就是"思考 → 行动 → 观察"三个动作的往返,退出条件是模型不再请求工具;约束有四件套——工具白名单、步数上限、token 预算、写操作 HITL;白名单校验在执行前做,模型请求的工具名是建议、代码里的白名单是决定;RAG 检索封装成工具,靠工具描述让模型按需触发。
评论区聊聊:
你手写过 Agent 吗?踩过哪个坑——死循环、越权工具还是 HITL 卡死?
下期预告:
Agent 的可观测性与成本控制
——trace、token 计量、熔断降级,把 Agent 关进监控的笼子里。关注我,AI 落地系列持续更新,只讲面试和实战都硬核的货。