Agent 进阶:Plan-and-Execute、确定性流程与生产级可靠性
导读:当 Agent 从 Demo 走向生产,你会发现 ReAct 模式并非万能——复杂任务容易"绕圈子",金额计算不能交给大模型拍脑袋,线上死循环一晚上就能烧光 Token 预算。本文将围绕四个核心主题展开:Plan-and-Execute 分步规划、确定性流程与 AI 决策的边界划分、生产级可靠性工程实践,以及一个真实的 智能销售数据分析 Agent 项目全貌。读完之后,你不仅能写出更聪明的 Agent,还能让它在生产环境里"稳如老狗"。
一、Plan-and-Execute:让 Agent 先想清楚再动手
1.1 ReAct 模式的天花板
我们之前学过 ReAct(Reasoning + Acting)模式:思考 -> 执行 -> 观察 -> 再思考……循环往复直到得到答案。对于三五步就能搞定的简单任务,ReAct 非常顺手。
但如果任务变成这样:
"帮我分析上个月各区域的销售数据,找出下滑超过 20% 的区域,再查一下这些区域竞品的活动,最后生成一份分析报告。"
ReAct 就有点力不从心了。因为它是一种边想边做的模式,每一步只看当前状态来决定下一步,缺少全局视图。碰到步骤多、有依赖关系的复杂任务,它很容易原地打转、反复绕圈。
1.2 Plan-and-Execute 的核心思路
Plan-and-Execute 的哲学很简单:先把整个任务想明白,拆解成有序的步骤,然后逐步执行。
你在用 ChatGPT、Claude 等工具时,可能注意到右上角会有一个 "thinking" 的过程——模型先梳理"用户要什么",然后列出"第一步做什么、第二步做什么、第三步做什么"。这就是 Plan-and-Execute 的体现。
1.3 两种模式对比
| 维度 | ReAct | Plan-and-Execute |
|---|---|---|
| 决策方式 | 每步基于上一步结果决定下一步 | 先生成完整计划,再逐步执行 |
| 全局视图 | 无,容易绕圈 | 有,步骤间依赖关系清晰 |
| 动态调整 | 天然支持 | 执行中可动态修改后续步骤 |
| 适用场景 | 3~5 步内、路径不确定、探索性任务 | 5 步以上、步骤有依赖、可并行、需展示进度 |
经验法则:拿到一个任务,先估算大概需要几步。步骤多就用 Plan-and-Execute;拿不准的先用 ReAct 跑一跑,看效果再决定。
1.4 核心组件:任务规划器(TaskPlanner)
Plan-and-Execute 的核心是一个专门负责任务拆解的 Planner。为什么要单独用一个模型(或单独的 Prompt)来做规划?因为规划和执行对模型的要求差异很大:
- 规划:需要全局视野,理解步骤间的依赖关系
- 执行:只需专注当前步骤,正确调用工具
分开处理,各自有针对性的 System Prompt,效果会好很多。
来看我们项目中 TaskPlanner 的实现(com.jichi.agent.service.TaskPlanner):
@Service
public class TaskPlanner {
private final ChatClient plannerClient;
public TaskPlanner(@Qualifier("dashScopeChatModel") ChatModel chatModel) {
this.plannerClient = ChatClient.builder(chatModel)
.defaultSystem("""
你是一个任务规划专家。
收到用户任务后,把它分解为具体的执行步骤。
输出格式(严格遵守):
STEP 1: [步骤描述]
STEP 2: [步骤描述]
STEP 3: [步骤描述]
...
要求:
- 每步描述要具体,说明做什么、用什么工具、期望结果
- 步骤数量合理,不要过于细碎(3-8 步为宜)
- 标注步骤依赖(如果某步依赖上一步的结果,说明"依赖 Step X")
- 只输出步骤,不要解释
""")
.build();
}
public List<String> plan(String task, List<String> availableTools) {
String toolsDescription = String.join("、", availableTools);
String planText = plannerClient.prompt()
.user("可用工具:" + toolsDescription + "\n\n任务:" + task)
.call()
.content();
return parsePlan(planText);
}
private List<String> parsePlan(String planText) {
return Arrays.stream(planText.split("\n"))
.filter(line -> line.matches("STEP \\d+:.*"))
.map(line -> line.replaceFirst("STEP \\d+:\\s*", "").strip())
.toList();
}
}
几个关键细节:
- 输出格式必须强制约束:如果不规定死
STEP N:这样的格式,大模型可能输出各种奇怪的格式,parsePlan解析时就会出问题。 - 步骤数控制在 3~8 步:让 AI 自由发挥,它可能给出 14 步,但到第 7 步就跑偏了。拆得太碎,模型反而会为了"凑步骤"而瞎搞。
- 工具列表必须传给规划器:
plan()方法接收availableTools参数,Planner 只有知道有哪些工具可用,才能合理安排步骤。否则它可能规划出一堆你根本没有的能力。
1.5 核心组件:步骤执行器(StepExecutor)
执行器的核心设计目标是:每次只处理当前步骤,但能看到之前所有步骤的执行结果。
为什么要带上下文?举个例子:Step 1 查到了上周销售额 50 万,Step 3 做环比计算时就需要用到这个 50 万。如果执行器每次都从零开始,信息链就断了。
来看 StepExecutor(com.jichi.agent.service.StepExecutor)的实现:
@Service
public class StepExecutor {
private final ChatClient executorClient;
public StepExecutor(@Qualifier("dashScopeChatModel") ChatModel chatModel,
AssistantTools assistantTools,
CustomerServiceTools customerServiceTools) {
this.executorClient = ChatClient.builder(chatModel)
.defaultSystem("""
你是一个步骤执行器。
你会收到一个具体的步骤描述,以及之前步骤的执行结果。
请使用合适的工具完成这个步骤,并返回执行结果。
如果这个步骤不需要工具,直接用你的知识完成并返回结果。
""")
.defaultTools(assistantTools, customerServiceTools)
.build();
}
/**
* 执行单个步骤
*
* @param step 步骤描述
* @param previousResults 之前各步骤的执行结果(key: 步骤编号, value: 结果)
*/
public String execute(String step, Map<Integer, String> previousResults) {
StringBuilder context = new StringBuilder();
if (!previousResults.isEmpty()) {
context.append("之前步骤的执行结果:\n");
previousResults.forEach((stepNo, result) ->
context.append("Step ").append(stepNo).append(" 结果:").append(result).append("\n"));
context.append("\n");
}
context.append("当前需要执行的步骤:").append(step);
return executorClient.prompt()
.user(context.toString())
.call()
.content();
}
}
注意两个设计点:一是 Executor 通过 .defaultTools() 挂载了实际的工具类,而 Planner 不挂工具——职责分离;二是 previousResults 使用 Map<Integer, String> 按步骤编号索引,传递给模型的上下文清晰有序。
1.6 控制器:PlanAndExecuteAgent 串联规划与执行
最终 PlanAndExecuteAgent(com.jichi.agent.service.PlanAndExecuteAgent)把 Planner 和 Executor 串起来:
@Service
public class PlanAndExecuteAgent {
private final TaskPlanner planner;
private final StepExecutor executor;
private static final List<String> AVAILABLE_TOOLS = List.of(
"getWeather(查天气)",
"getCurrentDateTime(查时间)",
"getExchangeRate(查汇率)",
"getOrderTracking(查订单状态)",
"searchProducts(搜索商品)"
);
public ExecutionResult run(String task) {
log.info("开始执行任务:{}", task);
List<String> steps = planner.plan(task, AVAILABLE_TOOLS);
log.info("生成计划,共 {} 步:{}", steps.size(), steps);
Map<Integer, String> results = new LinkedHashMap<>();
for (int i = 0; i < steps.size(); i++) {
String step = steps.get(i);
int stepNo = i + 1;
log.info("执行 Step {}:{}", stepNo, step);
try {
String result = executor.execute(step, results);
results.put(stepNo, result);
log.info("Step {} 完成", stepNo);
} catch (Exception e) {
log.error("Step {} 执行失败:{}", stepNo, e.getMessage());
results.put(stepNo, "执行失败:" + e.getMessage());
// 这里可以 break(失败就停)或 continue(跳过继续执行后续步骤)
// 鸡哥建议:非关键步骤 continue,关键步骤 break
}
}
return new ExecutionResult(task, steps, results);
}
public record ExecutionResult(
String originalTask,
List<String> plan,
Map<Integer, String> stepResults
) {
public String summary() {
StringBuilder sb = new StringBuilder();
sb.append("任务:").append(originalTask).append("\n\n");
sb.append("执行步骤:\n");
for (int i = 0; i < plan.size(); i++) {
int stepNo = i + 1;
sb.append("Step ").append(stepNo).append(":").append(plan.get(i)).append("\n");
sb.append("结果:").append(stepResults.getOrDefault(stepNo, "未执行")).append("\n\n");
}
return sb.toString();
}
}
}
这里用 LinkedHashMap 是有讲究的——它能保证步骤结果按插入顺序排列,传递给模型的上下文更加清晰。另一个额外的好处是:Plan 生成后可以快速返回给前端做进度条展示,用户能清楚看到"当前执行到第几步了",体验非常好。
1.7 避坑提醒
简单任务别往 Plan-and-Execute 里塞。两三步就能搞定的事情,多了一个规划环节反而更慢,还可能把简单任务过度复杂化。
二、确定性流程与 AI 决策:划清边界
2.1 一个真实的教训
曾经有一个团队在早期实践时,把退款审批全权交给了 AI。结果 AI 发现用户描述得特别可怜时,就开始批退款——原本不该批的退款也批了,直接损失两千多块。初期可以理解为经验不足,但如果持续这么做,那就是生产事故。
核心原则:涉及金钱、权限或不可逆操作的节点,必须用确定性代码控制。AI 只在理解自然语言、做判断分类、生成内容时介入。
2.2 清晰的责任划分
| 确定性代码负责 | AI 负责 |
|---|---|
| 权限校验 | 理解用户意图 |
| 金额计算(扣款、折扣、税率) | 内容分类(产品问题/物流问题/服务问题) |
| 状态流转(待支付 -> 已支付) | 生成文字回复 |
| 不可逆操作(删除数据、发短信) | 主观判断、模糊规则 |
| 高精度判断 | 需要理解上下文的决策 |
简单总结:规则明确、结果唯一的,用代码;规则模糊、需要理解上下文的,用 AI。
比如"退款期限是 7 天"——这个规则非常明确,结果只有"符合"或"不符合",就该用代码判断。不要让 AI 因为用户"装可怜"就网开一面。
2.3 状态机思维:设计混合工作流
在设计复杂的 Agent 流程时,推荐使用状态机来明确每个节点的属性——是 AI 节点还是确定性节点。我们项目里用枚举来定义工作流状态(com.jichi.agent.model.WorkflowState):
public enum WorkflowState {
START,
INTENT_CLASSIFICATION, // AI 节点:理解用户意图
AUTH_CHECK, // 确定性节点:权限校验
CONDITION_CHECK, // 确定性节点:业务规则判断(7 天期限等)
AMOUNT_CALCULATION, // 确定性节点:金额计算
EXECUTION, // 确定性节点:执行操作(扣款/退款)
AI_GENERATION, // AI 节点:生成回复内容
NOTIFICATION, // 确定性节点:发送通知
HUMAN_REVIEW, // 人工节点:大额或风险操作等待人工审批
COMPLETED,
FAILED
}
每个状态的注释标注了它是"AI 节点"、"确定性节点"还是"人工节点",一目了然。
2.4 实战:退款客服 Agent(RefundWorkflowService)
退款场景是一个非常典型的混合流程——既要理解用户自然语言(AI 擅长),又有金钱和业务规则(必须确定性)。来看项目中 RefundWorkflowService(com.jichi.agent.service.RefundWorkflowService)的完整实现:
第一步:AI 意图识别
注意构造函数中,这个 ChatClient 专门用于意图分类和内容生成,不挂任何工具——职责单一:
public RefundWorkflowService(@Qualifier("dashScopeChatModel") ChatModel chatModel,
OrderRepository orderRepository,
RefundService refundService,
NotificationService notificationService,
HumanInTheLoopService humanInTheLoopService) {
// 这个 ChatClient 专门用于意图分类和内容生成,不挂任何工具
// 职责单一:只做语言理解,不做任何业务操作
this.aiClient = ChatClient.builder(chatModel)
.defaultSystem("你是一个意图分类助手,只输出分类结果,不要解释。")
.build();
// ... 其他依赖注入
}
意图分类方法强制输出格式,防止 AI 自由发挥输出无法解析的内容:
// AI 节点:意图分类
private String classifyIntent(String message) {
return aiClient.prompt()
.user("""
分类用户意图,只输出以下之一(不要有其他文字):
FULL_REFUND(全额退款)
PARTIAL_REFUND(部分退款)
EXCHANGE(换货)
COMPLAINT(投诉,不需要退款)
用户说:""" + message)
.call()
.content()
.strip();
}
第二步:确定性校验——权限和订单
// ——— 确定性节点:权限和订单校验 ———
// 这两步绝对不能交给 AI:AI 可能被"我真的很需要这笔钱"这种描述影响判断
Order order = orderRepository.findById(orderId).orElse(null);
if (order == null) {
return WorkflowResult.fail("订单不存在:" + orderId);
}
if (!order.getUserId().equals(userId)) {
return WorkflowResult.fail("无权操作此订单");
}
第三步:确定性校验——业务规则
// 确定性节点:退款资格校验
private RefundEligibility checkEligibility(Order order) {
long daysSincePurchase = ChronoUnit.DAYS.between(
order.getCreatedAt().toLocalDate(), LocalDate.now());
if (daysSincePurchase > 7) {
return RefundEligibility.deny(
"已超过 7 天无理由退货期限(购买于 " + daysSincePurchase + " 天前)");
}
if ("SHIPPED".equals(order.getStatus()) && order.getSignedAt() == null) {
return RefundEligibility.deny("商品正在配送中,请收货后申请退款");
}
if ("REFUNDED".equals(order.getStatus())) {
return RefundEligibility.deny("订单已退款,不可重复申请");
}
return RefundEligibility.pass();
}
第四步:确定性计算——退款金额
用 switch 表达式根据意图计算金额,精确可控:
// 确定性节点:退款金额计算
private double calculateRefundAmount(Order order, String intent) {
return switch (intent) {
case "FULL_REFUND", "EXCHANGE" -> order.getActualAmount();
case "PARTIAL_REFUND" -> order.getActualAmount() * 0.5;
default -> order.getActualAmount();
};
}
第五步:人工审批节点(大额退款)
规则明确(> 1000 元),触发条件是确定性的;但审批决定权交给人类:
// ——— 人工审批节点:大额退款需要主管确认 ———
if (refundAmount > 1000) {
String workflowId = UUID.randomUUID().toString();
humanInTheLoopService.requestApproval(
workflowId,
"大额退款申请:¥" + String.format("%.2f", refundAmount) + ",订单:" + orderId,
"supervisor@company.com",
Map.of("orderId", orderId, "userId", userId, "amount", refundAmount)
);
return WorkflowResult.pending("退款申请已提交,金额较大需人工审核,预计 1 小时内处理");
}
第六步:执行退款 + AI 生成友好回复 + 确定性通知
// ——— 确定性节点:执行退款(≤ 1000 元直接处理)———
refundService.process(orderId, refundAmount);
// ——— AI 节点:生成用户友好的确认回复 ———
String reply = generateApprovalReply(order, refundAmount);
// ——— 确定性节点:发送通知 ———
notificationService.sendRefundNotification(userId, orderId, refundAmount);
return WorkflowResult.success(reply, refundAmount);
其中 AI 生成回复的方法,AI 只负责"怎么说",不决定"能不能退":
private String generateApprovalReply(Order order, double refundAmount) {
return aiClient.prompt()
.system("你是一个专业、友善的客服")
.user(String.format("退款已通过。订单 %s,退款金额 ¥%.2f,请生成简短的确认回复,让用户知道款项会在 3-5 个工作日内到账",
order.getId(), refundAmount))
.call()
.content();
}
2.5 Human-in-the-Loop:人工审批节点
来看 HumanInTheLoopService(com.jichi.agent.support.HumanInTheLoopService)的实现:
@Service
public class HumanInTheLoopService {
private final PendingApprovalRepository approvalRepo;
private final NotificationService notificationService;
/**
* 高风险操作暂停,等待人工审批
* 触发场景:大额退款、批量操作、敏感数据修改
*/
public void requestApproval(String workflowId, String description,
String approverEmail, Object payload) {
PendingApproval approval = new PendingApproval(
workflowId, description, approverEmail,
payload, "PENDING", null, LocalDateTime.now());
approvalRepo.save(approval);
// 通知审批人——这一步是确定性的,必须发
notificationService.notifyApprover(approverEmail,
"需要你审批:" + description +
"\n审批链接:https://admin/approve/" + workflowId);
}
/** 审批通过,恢复流程继续执行 */
public void onApproved(String workflowId) {
approvalRepo.updateStatus(workflowId, "APPROVED", null);
// 实际项目这里触发工作流引擎恢复(Activiti / Flowable 等)
}
/** 审批拒绝,终止流程并通知用户 */
public void onRejected(String workflowId, String reason) {
approvalRepo.updateStatus(workflowId, "REJECTED", reason);
}
/** 查询审批状态(供轮询) */
public String getStatus(String workflowId) {
return approvalRepo.findById(workflowId)
.map(PendingApproval::status)
.orElse("NOT_FOUND");
}
}
在生产环境中,通常会结合公司的 OA 审批系统来实现;教学场景下可以用轮询接口来查询审批状态。
2.6 切忌一刀切
千万不要走两个极端:
- 全交给 AI:迟早出事故,金钱、权限相关的操作一旦被绕过,损失不可逆。
- 全用确定性代码:你搞不定自然语言的意图识别和灵活的内容生成。
正确做法永远是混合使用,让 AI 和确定性代码各司其职。
三、Agent 生产级可靠性:上线前的"生死清单"
Agent 在本地跑得好好的,上了生产就暴露各种问题。以下是实战中总结的几个关键防护措施。
3.1 防死循环:最大调用次数限制(MaxIterationsAdvisor)
Agent 最可怕的事情之一:模型反复调用同一个工具,拿不到想要的答案,或者拿到了也不结束,就在那转圈。工具返回"查询失败,请重试",模型就真的重试了——又失败又重试,Token 耗尽才停下来。
解决方案:基于 Spring AOP 拦截器,为所有 @Tool 方法设置调用上限。来看 MaxIterationsAdvisor(com.jichi.agent.advisor.MaxIterationsAdvisor):
/**
* 用 Spring AOP 拦截所有 @Tool 方法,统计当前请求的工具调用次数。
* 比实现 Spring AI 内部 Advisor 接口更稳定,不依赖框架私有 API。
*/
@Aspect
@Component
public class MaxIterationsAdvisor {
private static final int DEFAULT_MAX_ITERATIONS = 10;
// ThreadLocal 保证同一请求内的计数互不干扰
private static final ThreadLocal<AtomicInteger> CALL_COUNT =
ThreadLocal.withInitial(AtomicInteger::new);
/** 每次新 Agent 任务开始前调用,重置计数器 */
public static void reset() {
CALL_COUNT.remove();
}
/** 拦截项目内所有标注了 @Tool 的方法 */
@Around("@annotation(org.springframework.ai.tool.annotation.Tool)")
public Object limitToolCalls(ProceedingJoinPoint joinPoint) throws Throwable {
int count = CALL_COUNT.get().incrementAndGet();
if (count > DEFAULT_MAX_ITERATIONS) {
CALL_COUNT.remove();
throw new AgentMaxIterationsException(
"Agent 超过最大工具调用次数(" + DEFAULT_MAX_ITERATIONS + "),任务中止");
}
return joinPoint.proceed();
}
}
自定义异常类 AgentMaxIterationsException 很简洁:
public class AgentMaxIterationsException extends RuntimeException {
public AgentMaxIterationsException(String message) {
super(message);
}
}
调用次数上限经验值:
- 简单任务:3~5 次
- 分析类任务:8~12 次
- 超过 15 次:考虑是不是该把任务拆开
3.2 超时控制 + 统一错误处理:SafeAgentController
工具调用可能因为数据库慢查询、第三方 API 超时、网络抖动等原因卡住。来看 SafeAgentController(com.jichi.agent.controller.SafeAgentController)如何实现三层防护——超时、死循环、预算:
@RestController
@RequestMapping("/api/agent")
public class SafeAgentController {
private final PersonalAssistantAgent agent;
@PostMapping("/chat")
public ResponseEntity<AgentResponse> chat(@RequestBody ChatRequest request) {
try {
// 套一层 CompletableFuture,统一控制整体任务超时
String result = CompletableFuture
.supplyAsync(() -> agent.chat(request.message()))
.get(60, TimeUnit.SECONDS);
return ResponseEntity.ok(AgentResponse.success(result));
} catch (AgentMaxIterationsException e) {
return ResponseEntity.ok(AgentResponse.error(
"这个问题比较复杂,建议拆分成几个小问题分别来问",
"MAX_ITERATIONS"));
} catch (TimeoutException e) {
return ResponseEntity.ok(AgentResponse.error(
"处理时间较长,已超出等待上限,请稍后重试或尝试更简单的问题",
"TIMEOUT"));
} catch (Exception e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
if (cause instanceof AgentMaxIterationsException) {
return ResponseEntity.ok(AgentResponse.error(
"这个问题比较复杂,建议拆分成几个小问题分别来问",
"MAX_ITERATIONS"));
}
if (cause.getMessage() != null && cause.getMessage().contains("Token 预算")) {
return ResponseEntity.ok(AgentResponse.error(
"这个问题处理量超出单次限额,请拆分成更小的问题",
"BUDGET_EXCEEDED"));
}
return ResponseEntity.status(500)
.body(AgentResponse.error("服务暂时开了个小差,请稍后重试", "INTERNAL_ERROR"));
}
}
record AgentResponse(boolean success, String content, String errorCode) {
static AgentResponse success(String content) {
return new AgentResponse(true, content, null);
}
static AgentResponse error(String message, String code) {
return new AgentResponse(false, message, code);
}
}
}
注意一个细节:超时和死循环都返回 200 状态码 + 友好提示,不要抛 500 给前端。用户看到"服务器内部错误"只会一脸懵,但看到"建议拆分成几个小问题"就知道该怎么办了。同时 AgentResponse 带有 errorCode(MAX_ITERATIONS、TIMEOUT、BUDGET_EXCEEDED),前端可以根据不同错误码做不同的 UI 提示。
3.3 Token 成本控制(TokenUsageLogger)
Agent 的多轮调用会快速消耗 Token。必须加监控 + 预算限制。来看 jc-sales-agent 项目中 TokenUsageLogger(com.jichi.salesAgent.config.TokenUsageLogger)的实现:
@Component
@Slf4j
public class TokenUsageLogger implements ChatModelListener {
private final Counter inputTokenCounter;
private final Counter outputTokenCounter;
public TokenUsageLogger(MeterRegistry meterRegistry) {
this.inputTokenCounter = Counter.builder("llm.tokens.input")
.description("Input tokens consumed")
.register(meterRegistry);
this.outputTokenCounter = Counter.builder("llm.tokens.output")
.description("Output tokens consumed")
.register(meterRegistry);
}
@Override
public void onResponse(ChatModelResponseContext responseContext) {
var usage = responseContext.chatResponse().tokenUsage();
if (usage != null) {
int input = usage.inputTokenCount() != null ? usage.inputTokenCount() : 0;
int output = usage.outputTokenCount() != null ? usage.outputTokenCount() : 0;
inputTokenCounter.increment(input);
outputTokenCounter.increment(output);
// 估算费用(qwen-max 价格:输入 0.04 元/千Token,输出 0.12 元/千Token)
double cost = input * 0.04 / 1000.0 + output * 0.12 / 1000.0;
log.info("Token 用量 | 输入:{} | 输出:{} | 本次费用约:¥{}",
input, output, String.format("%.4f", cost));
}
}
}
它实现了 LangChain4j 的 ChatModelListener 接口,通过 Micrometer 的 Counter 将 Token 指标注册到监控系统。每次模型调用完成后自动记录输入/输出 Token 数并估算费用。这些指标可以接入 Prometheus + Grafana 做实时监控和告警。
预算怎么定? 先跑一批正常任务,看看平均消耗多少 Token,然后乘以 1.5~2 倍作为上限。
3.4 幻觉防护:HallucinationGuard
Agent 有一个隐蔽的陷阱:它可能假装调了工具,然后在回答里编造数据。比如让它查某商品上周的销量,它写了一个具体数字,看起来很真,但其实是瞎编的。如果这些数据流入报表,后果不堪设想。
解决方案:用一个轻量级模型做二次校验。来看 HallucinationGuard(com.jichi.agent.support.HallucinationGuard):
@Component
public class HallucinationGuard {
private final ChatClient verifierClient;
public HallucinationGuard(@Qualifier("dashScopeChatModel") ChatModel chatModel) {
// 用轻量模型做验证,速度快成本低
this.verifierClient = ChatClient.builder(chatModel)
.defaultSystem("""
你是一个数据一致性检查员。
检查 Agent 的回答是否和工具实际返回的数据一致。
如果回答里出现了工具数据里没有的具体数字或事实,判定为不一致。
只输出 CONSISTENT 或 INCONSISTENT: [不一致的具体内容]
""")
.build();
}
/**
* 检查最终答案是否和工具数据一致
*
* @param toolResults 工具实际返回的数据(从日志或拦截层收集)
* @param finalAnswer Agent 生成的最终答案
*/
public boolean isConsistent(String toolResults, String finalAnswer) {
String result = verifierClient.prompt()
.user("工具实际返回的数据:\n" + toolResults + "\n\nAgent 的回答:\n" + finalAnswer)
.call()
.content()
.strip();
return result.startsWith("CONSISTENT");
}
}
幻觉检测不需要每条都做,只针对高精度要求的场景:财务报告、库存数字、价格信息等。普通问答类无所谓。
3.5 上线前 Checklist
在 Agent 上线前,务必逐条检查:
- [ ] 最大工具调用次数是否设置(MaxIterationsAdvisor)
- [ ] 单个工具是否有超时保护
- [ ] 整体任务是否有超时限制(CompletableFuture + get(timeout))
- [ ] Token 消耗是否有监控和预算(TokenUsageLogger)
- [ ] 写操作是否有权限校验
- [ ] 工具返回异常是否有兜底处理
- [ ] 请求日志和降级策略是否就绪
- [ ] 连续失败场景是否有熔断机制
- [ ] 是否有明确的 Prompt 告知模型"不能做什么"
四、实战项目:智能销售数据分析 Agent(jc-sales-agent)
4.1 项目背景
假设你入职了一家电商公司,老板每天要看销售数据。传统方式是:老板提需求 -> 开发写接口 -> 测试 -> 部署,快则半天,慢则一两周。而且老板的需求永远在变——今天要区域对比,明天要销售排名,后天要异常检测,接口永远写不完。
有了 AI Agent 之后,老板直接用自然语言提问,Agent 自动分析并返回结果,甚至还能生成图表。
4.2 核心能力
这个项目能做到以下几件事:
-
追问下钻(多轮对话):用户先问"上个月华东区的销售情况",再追问"张伟和王芳各做了多少",接着问"张伟这个月有没有异常"——Agent 能保持上下文连续追问。
-
多步自主推理:问"本季度销量最大的销售员是谁",Agent 会自主查排名、对比数据、分析历史趋势,最后给出结论和可能的原因分析。
-
图表生成:用户说"帮我画一张近六个月各大区的销售趋势折线图",Agent 调用工具生成可视化图表。
-
主动预警:"最近有没有什么风险?"——Agent 扫描数据,按高、中、低优先级输出预警信息。
4.3 SalesAgent 接口定义
来看 Agent 的核心接口(com.jichi.salesAgent.agent.SalesAgent),使用 LangChain4j 的 @SystemMessage 注入业务规则和权限边界:
public interface SalesAgent {
@SystemMessage("""
你是一个专业的销售数据分析助手,服务于销售团队。
【当前时间】今天是 {{today}}。
请严格基于此日期理解所有时间相关词语:
- "今天/当前" = {{today}}
- "本月" = {{today}} 所在的自然月(1日至月末)
- "上个月" = {{today}} 所在月的上一个自然月
- "本季度" = {{today}} 所在季度(Q1:1-3月, Q2:4-6月, Q3:7-9月, Q4:10-12月)
- "今年" = {{today}} 所在年份的 1月1日 至 12月31日
- "近N个月" = 从 {{today}} 往前推 N 个自然月
你的能力:
- 查询销售订单数据
- 计算销售汇总统计(总额、排名、Top N)
- 分析同比环比趋势
- 生成图表数据(ECharts JSON 格式)
- 检测销售数据异常
你的限制(严格遵守):
- 只能查询数据,不能修改任何数据
- 不能预测未来销售(没有预测能力)
- 不能发送邮件、通知等操作
- 如果问题超出能力范围,请明确告知并说明原因
回答要求:
- 用中文回答
- 数据用具体数字,金额格式化为 ¥X,XXX
- 有数据时给出简短的分析判断,不要只是罗列数据
- 发现数据异常时主动提醒
""")
String chat(@MemoryId String sessionId, @UserMessage String message, @V("today") String today);
}
几个值得注意的设计:@MemoryId 标注 sessionId 支持多轮对话;@V("today") 模板变量让模型能正确理解"本月""上个月"等时间概念;System Prompt 明确列出了能力和限制,防止 Agent 越权操作。
4.4 Agent 装配:SalesAgentConfig
Agent 实例由 SalesAgentConfig(com.jichi.salesAgent.agent.SalesAgentConfig)手动创建 Bean,不依赖自动装配:
@Configuration
@RequiredArgsConstructor
public class SalesAgentConfig {
private final ChatModel chatLanguageModel;
private final StreamingChatModel streamingChatLanguageModel;
private final SalesQueryTool salesQueryTool;
private final SalesSummaryTool salesSummaryTool;
private final SalesTrendTool salesTrendTool;
private final ChartGeneratorTool chartGeneratorTool;
private final AnomalyDetectionTool anomalyDetectionTool;
private final MysqlChatMemoryStore chatMemoryStore;
@Bean
public SalesAgent salesAgent() {
return AiServices.builder(SalesAgent.class)
.chatModel(chatLanguageModel)
.streamingChatModel(streamingChatLanguageModel)
.tools(salesQueryTool,
salesSummaryTool,
salesTrendTool,
chartGeneratorTool,
anomalyDetectionTool)
.beforeToolExecution(exec ->
log.info("▶ 工具调用开始 | 工具:{} | 参数:{}",
exec.request().name(),
exec.request().arguments()))
.afterToolExecution(exec ->
log.info("◀ 工具调用完成 | 工具:{} | 结果长度:{} 字符",
exec.request().name(),
exec.result() != null ? exec.result().length() : 0))
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20)
.chatMemoryStore(chatMemoryStore)
.build())
.build();
}
}
5 个工具对象注入后由 AiServices.builder() 统一注册,.beforeToolExecution() 和 .afterToolExecution() 钩子记录每次工具调用的入参和结果长度,方便排查问题。chatMemoryProvider 配合 MysqlChatMemoryStore 实现了对话历史的持久化存储。
4.5 工具层设计亮点
以 SalesQueryTool(com.jichi.salesAgent.tool.SalesQueryTool)为例,看看工具描述的设计技巧:
@Tool("查询原始销售订单数据。适用于:查具体订单、看某时段订单列表、统计某时段订单总数。" +
"【不适合】排名、增长率、图表生成、异常检测等场景,那些请使用对应的专用工具。")
public String queryOrders(
@P("查询开始日期,格式 yyyy-MM-dd,如 2024-11-01") String startDate,
@P("查询结束日期,格式 yyyy-MM-dd,如 2024-11-30") String endDate,
@P("大区名称,如:华东区、华南区、华北区、西南区。传 null 或空字符串表示查全公司") String regionName,
@P("销售员姓名,如需按特定销售员筛选则传入,如:张磊。否则传 null 或空字符串") String repName,
@P("最多返回条数,默认 20,最大 50。避免返回数据过多") int limit) {
// ...
}
工具描述里不仅说了"适用于"什么场景,还明确标注了"不适合"什么场景——这能有效避免模型选错工具。每个参数的 @P 描述都给出了格式示例和默认行为说明。
再看异常检测工具 AnomalyDetectionTool(com.jichi.salesAgent.tool.AnomalyDetectionTool),它无需任何参数,自动全面扫描四类异常:
@Tool("自动检测销售数据中的所有异常,包括:大区订单量骤降、产品连续零销售、" +
"销售员退单率异常、销售员业绩骤降。适用于:有没有异常、风险排查、预警检测等场景。" +
"无需传入参数,系统自动全面扫描。")
public String detectAllAnomalies() {
List<AnomalyDTO> anomalies = new ArrayList<>();
anomalies.addAll(detectRegionDropAnomalies()); // 大区订单量骤降
anomalies.addAll(detectZeroSaleProducts()); // 产品连续零销售
anomalies.addAll(detectHighRefundReps()); // 销售员退单率异常
anomalies.addAll(detectRepPerformanceDrop()); // 销售员业绩骤降
// 按优先级排序:HIGH > MEDIUM > LOW
anomalies.sort((a, b) -> severityOrder(a.severity()) - severityOrder(b.severity()));
// ... 格式化输出
}
4.6 输入安全:ToolInputValidator
为了防止 AI 传入恶意或无效参数,ToolInputValidator(com.jichi.salesAgent.security.ToolInputValidator)对所有工具输入做白名单校验:
@Component
public class ToolInputValidator {
private static final Set<String> VALID_REGIONS =
Set.of("华东区", "华南区", "华北区", "西南区");
private static final Set<String> VALID_CHART_TYPES =
Set.of("line", "bar", "pie");
private static final Set<String> VALID_DIMENSIONS =
Set.of("region", "rep", "category");
public String validateRegionName(String regionName) {
if (regionName == null || regionName.isBlank()) return null;
if (!VALID_REGIONS.contains(regionName)) {
throw new IllegalArgumentException("无效的大区名称:" + regionName +
",有效值为:" + VALID_REGIONS);
}
return regionName;
}
public int validateTopN(int topN) {
return Math.min(Math.max(Math.abs(topN), 1), 20);
}
// ... 其他校验方法
}
大区名称、图表类型、维度等全部走白名单,日期走正则 + LocalDate.parse() 双重校验。这在防 SQL 注入和参数越界方面非常关键。
4.7 会话持久化:MysqlChatMemoryStore
对话历史落 MySQL,支持断点续聊(com.jichi.salesAgent.memory.MysqlChatMemoryStore):
@Component
@RequiredArgsConstructor
public class MysqlChatMemoryStore implements ChatMemoryStore {
private final ChatMemoryRepository repository;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String sessionId = memoryId.toString();
return repository.findBySessionId(sessionId)
.map(entity -> {
try {
return ChatMessageDeserializer.messagesFromJson(entity.getMessages());
} catch (Exception e) {
log.warn("反序列化对话记忆失败,sessionId={}", sessionId, e);
return Collections.<ChatMessage>emptyList();
}
})
.orElse(Collections.emptyList());
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String sessionId = memoryId.toString();
String json = ChatMessageSerializer.messagesToJson(messages);
ChatMemoryEntity entity = repository.findBySessionId(sessionId)
.orElseGet(() -> {
ChatMemoryEntity e = new ChatMemoryEntity();
e.setSessionId(sessionId);
return e;
});
entity.setMessages(json);
repository.save(entity);
}
@Override
public void deleteMessages(Object memoryId) {
repository.deleteBySessionId(memoryId.toString());
}
}
用 LangChain4j 内置的 ChatMessageSerializer/ChatMessageDeserializer 做 JSON 序列化,配合 JPA 的 ChatMemoryRepository 实现增删改查。反序列化失败时返回空列表而不是抛异常,保证服务可用性。
4.8 与传统方式的本质区别
| 传统方式 | Agent 方式 |
|---|---|
| 每个业务问题对应一个接口 | 5 个工具覆盖所有查询场景 |
| 新需求 = 新代码 | 新问题不需要写代码 |
| 展示什么写死在代码里 | 模型自主推理决定分析角度 |
Agent 通过工具化实现灵活性:SalesQueryTool 查原始订单、SalesSummaryTool 算排名汇总、SalesTrendTool 算同比环比、ChartGeneratorTool 生成 ECharts 图表、AnomalyDetectionTool 主动扫描异常——这 5 个工具组合起来,就能覆盖老板几乎所有的即兴提问。
五、总结
本文从四个维度梳理了 Agent 从"能跑"到"能用"的进阶路径:
-
Plan-and-Execute 解决了 ReAct 在复杂任务中"没有全局视图"的问题。通过独立的
TaskPlanner和StepExecutor,先规划再执行,步骤之间能传递上下文,还能动态调整计划。但记住:简单任务不要过度工程化。 -
确定性流程与 AI 决策的边界 是生产环境中最重要的设计原则之一。
RefundWorkflowService展示了如何让金钱、权限、不可逆操作走确定性代码,AI 只负责理解语言和生成内容。用WorkflowState状态机思维来设计混合工作流,每个节点的属性一目了然。 -
生产级可靠性 不是锦上添花,而是上线前的"生死线"。
MaxIterationsAdvisor防死循环、SafeAgentController双层超时 + 统一错误处理、TokenUsageLoggerToken 预算监控、HallucinationGuard幻觉校验——这些都是真金白银买来的教训。上线前对着 Checklist 逐条过一遍,能省去大量线上救火的时间。 -
智能销售分析 Agent(jc-sales-agent) 展示了如何将上述所有知识融合到一个真实项目中。通过
SalesAgent接口 +AiServices.builder()装配 + 5 个专用工具类的架构,替代传统的"一个需求一个接口"模式,让 Agent 具备追问下钻、多步推理、图表生成和主动预警的能力,同时通过ToolInputValidator、MysqlChatMemoryStore、TokenUsageLogger兼顾安全、持久化和监控等生产要求。
Agent 开发的门槛正在快速降低,但从 Demo 到生产的鸿沟依然存在。掌握了本文这些工程实践,你就有了跨越这道鸿沟的底气。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)