这两年一直致力于公司的Agent开发。总结了一些思考和解决方案,今天给大家分享下,也是给自己的一个复盘。
做 Agent 最有意思的是,通常是 Demo 刚跑通的那一下:模型理解了需求,自己选了工具,还给出了看起来不错的结果。
真正让人头疼的是,是第二个阶段。
用户换了一种问法,状态丢了;工具返回一个含糊的错误,Agent 开始乱试;你改了提示词,一个场景好了,另一个场景坏了;上线第二天,账单和延迟一起超预期。
所以,Agent Demo 和 Agent 系统之间,隔着一层工程问题。这就是通常所说的工程化解决方案
这层问题不是换框架能解决的。框架解决的是调用模型、注册工具、组织消息这些胶水层;真正难的是下面五件事:
状态怎么传?
工具怎么约束?
错误怎么恢复?
效果怎么回归?
延迟和成本怎么控?
LangChain、LangGraph、Spring AI、AgentScope 或自研循环,这些都是我们去做一个agent的基本框架,工程化的问题,他们依然存在。
可能这里有人嘴犟了,推出市面上一些比较好的开源框架 比如字节的deerflow,这个我们公司也在用,的确把一些基本上工程问题化解掉了,但就像你做springboot、cloud开发一样。你要知道怎么解决实际业务问题,实际的业务开发才是硬道理。
今天这篇文章带你从难点展开。每一节都先讲麻烦在哪里,再给可落地的解法。不管你是否搞过agent开发,都能明白。(多说一句,不用在乎什么语言,不管是Python、亦或是Spring ai生态,其实都是一致的,我们需要掌握的是解决思路与方案)
一、上下文:难在状态会丢
首当其冲肯定是上下文,上下文的难点:不是写 Prompt;
假设我们要做一个"汽车门店销量助手"。
用户先问:"帮我看看上海上周的销量。"
下一句又问:"那杭州呢?"
人一听就懂:用户要查"杭州上周的销量",继承"上周"和"销量",只把"上海"换成"杭州"。
但如果系统只把最后一句"那杭州呢?"传给模型,模型就不知道该查销量,更不知道时间范围是上周。
很多人的第一反应是把更多资料塞给模型。这样做短期有效,长期会引入三个问题:
状态丢失:用户说"换成上周""再看另一个对象",关键信息在历史对话里,不在当前输入里。
状态冲突:系统提示说只能查最近 30 天,历史里还有用户之前查过的去年数据。
重点稀释:工具返回、检索片段、执行日志全部塞进上下文,模型反而找不到关键约束。
Agent 的上下文不是一个聊天记录列表,而是一个随任务推进不断变化的运行时状态。
正确做法类似 Java 里的领域对象:维护一个明确的查询状态。
ini
class SalesQueryState {
private String city = "上海";
private LocalDate startDate = LocalDate.of(2026, 9, 14);
private LocalDate endDate = LocalDate.of(2026, 9, 20);
private String metric = "sales";
}
用户说"那杭州呢",只更新一个字段:
arduino
state.setCity("杭州");
不要把整个对象重建,也不要让模型随意覆盖所有字段。
一个实用的上下文结构是:
State 当前任务状态:城市、时间、指标
Evidence 本次回答需要的证据:销量数据、报表片段
History 最近几轮对话,用来理解"那杭州呢"
一句话:程序维护状态,模型解析意图。
落成流程就是四步:
- 模型读取最近对话 + 当前状态,输出结构化意图
- 程序校验城市、日期、指标是否合法
- 只合并允许变化的字段
- 用新状态去查数据 对应 Java 大概是:
java
QueryIntent intent = llm.parseIntent(history, state, "那杭州呢?");
if (!cityRepository.exists(intent.city())) {
throw new InvalidCityException(intent.city());
}
state.apply(intent); // 只改 city,保留时间和指标
二、工具:难在把概率系统接到确定性接口上
工具调用看起来只是让模型调用函数,麻烦在失败场景。
比如模型调用:
scss
querySales("魔都", lastWeek);
系统可能遇到:
魔都不是标准城市名
日期算错
查询结果为空
接口超时
用户没有权限
如果工具只返回字符串:
java
String querySales(String city, DateRange range);
模型分不清空结果、参数错、超时和权限问题,只能靠猜。
比如可以像设计 Java 接口一样设计工具契约:
java
record ToolResult(
Status status,
Map<String, Object> data,
String message,
boolean retryable,
String nextAction
) {}
enum Status {
SUCCESS, EMPTY, INVALID_ARGUMENT,
PERMISSION_DENIED, TIMEOUT
}
城市名错误时返回:
java
new ToolResult(
INVALID_ARGUMENT,
Map.of(),
"city 不是标准城市名",
true,
"调用 resolveCity,把魔都解析成上海"
);
这样程序知道能不能重试,模型知道下一步该做什么。
工具层还要做三件事:
- 参数校验:城市、日期、范围在工具层强制检查
- 幂等:下单、发消息、删除数据必须带幂等键
- 权限分级:只读、可逆写、不可逆写分开处理
一句话:不要指望 Prompt 保证安全,接口必须能挡住错误。
写工具时可以直接套这个模板:
java
ToolResult querySales(String city, DateRange range, User user) {
if (!permissionService.canReadSales(user, city)) {
return ToolResult.denied("无权限查看该城市销量");
}
String normalizedCity = cityService.normalize(city);
if (normalizedCity == null) {
return ToolResult.invalidArgument(
"城市名不标准",
"请先调用 resolveCity"
);
}
if (!dateService.isValidSalesRange(range)) {
return ToolResult.invalidArgument("日期超出可查范围", "请改成最近90天");
}
List<SalesRow> rows = salesRepository.findByCityAndRange(normalizedCity, range);
if (rows.isEmpty()) {
return ToolResult.empty("查询成功但没有数据", "不要编造趋势");
}
return ToolResult.success(rows);
}
重点是顺序不能乱:
先权限,再参数,再查询,最后处理空结果和异常。
三、执行:难在 Agent 越跑越偏
没有边界的 Agent 很容易这样跑:
第 1 步:查上海销量
第 2 步:查杭州销量
第 3 步:发现杭州门店数据异常
第 4 步:跑去查库存
第 5 步:又查了一遍上海
第 6 步:宣布任务完成
用户只是想比较上海和杭州上周销量,Agent 却发散到库存,还重复查询。
解决方案不是让模型"更认真",而是给它一个有限任务图:
ClarifyIntent 明确城市、时间、指标
ResolveCity 处理城市别名
FetchSales 查询销量
Analyze 对比结果
GenerateResult 生成回答
ConfirmResult 验收
类似 Java 状态机:
java
if (state == RESOLVE_CITY && cityValid) {
state = FETCH_SALES;
}
再加上预算:
最多 2 次销量查询 最多 1 次城市解析 最多 1 次重试 总耗时不超过 10 秒 最后由程序验收:
scss
boolean acceptable =
result.contains("上海")
&& result.contains("杭州")
&& result.timeRangeIs(lastWeek)
&& result.referencesToolData();
不要相信"模型说完成"。
一句话:模型负责决策建议,程序负责边界和验收。
可以直接落成三个对象。
第一,节点定义:
java
enum Node {
CLARIFY_INTENT,
RESOLVE_CITY,
FETCH_SALES,
ANALYZE,
GENERATE_RESULT,
CONFIRM_RESULT
}
第二,预算控制:
java
class TaskBudget {
private int llmCalls;
private int toolCalls;
private int retries;
boolean allowToolCall() {
return toolCalls < 5;
}
boolean allowRetry() {
return retries < 2;
}
}
第三,验收器:
java
class SalesResultValidator {
boolean validate(SalesQueryState state, AgentResult result) {
return result.cities().equals(state.cities())
&& result.range().equals(state.range())
&& result.metric().equals(state.metric())
&& result.allNumbersHaveSource();
}
}
如果验收失败:
第一次:把失败原因交给模型修复
第二次:降低任务范围
第三次:转人工或返回失败原因
四、评估:难在不知道错在哪一步
Agent 最后回答:
杭州销量比上海高 18%。
这个结论可能错在四层:
erlang
意图层:用户问门店销量,Agent 查了城市总量
工具层:只查了杭州,没查上海
数据层:把上周日期算错
生成层:数据是 8%,写成 18%
只看最终答案,无法定位问题。
所以要分层评估:
json
[
{
"id": "followup-city",
"history": ["帮我看看上海门店上周的销量"],
"input": "那杭州呢?",
"expect_action": "query_sales",
"expect_city": "杭州",
"expect_period": "last_week"
},
{
"id": "empty-result",
"simulate": "杭州无销量数据",
"expect_behavior": "说明无数据,不要编造趋势"
},
{
"id": "timeout",
"simulate": "销量接口超时",
"expect_behavior": "重试一次后返回部分结果"
}
]
同时记录执行轨迹:
意图解析结果
调用了哪个工具
参数是什么
工具返回什么状态
耗时多少
最终答案引用了哪些数据
这就像 Java 服务的调用链日志。没有 Trace,线上问题只能靠猜。
一句话:最终答案要看,但中间轨迹更能定位问题。
落地时只需要三张表。
第一张,意图用例:
输入:那杭州呢?
期望:query_sales / 杭州 / 上周 / sales
第二张,工具用例:
模拟:杭州接口超时
期望:重试一次,然后返回部分结果
禁止:编造杭州销量
第三张,结果用例:
必须包含:上海、杭州、上周、销量
所有数字必须来自工具返回
空数据必须明说
再把轨迹记下来:
ini
trace.recordIntent(intent);
trace.recordToolCall(toolName, args, status, latency);
trace.recordFinalResult(result);
上线前跑同一批用例,对比改动前后:
意图成功率
工具调用成功率
空数据处理是否合规
平均延迟
平均成本
五、成本和延迟:难在路径会放大
用户只问一句:
上海和杭州上周销量哪个更好?
系统实际可能做了 8 件事:
解析意图
解析城市别名
查上海
查杭州
杭州超时重试
组装上下文
生成总结
验收结果
如果全部串行,用户等待时间就是所有步骤相加。
上海和杭州两个查询互不依赖,应该并行:
ini
var shanghai = executor.submit(() -> querySales("上海", lastWeek));
var hangzhou = executor.submit(() -> querySales("杭州", lastWeek));
SalesResult left = shanghai.get();
SalesResult right = hangzhou.get();
再配合四个手段:
模型分级:分类用小模型,总结用强模型
Prompt 缓存:稳定前缀放前面,变化内容放后面
结果缓存:昨日销量不要每次重新算
最坏预算:限制调用次数、重试次数、token 和费用
一句话:优化成本前先看路径,不要只看单次模型调用。
实际落地可以按五步做。
- Trace 记录每步耗时和 token
- 无依赖调用并行
- 按任务选择模型
- 能缓存的先缓存
- 设置硬预算 模型路由不用复杂:
arduino
Model selectModel(Task task) {
if (task.type() == CLASSIFICATION || task.type() == FIELD_EXTRACTION) {
return Model.LIGHT;
}
if (task.risk() == HIGH || task.needsLongContext()) {
return Model.STRONG;
}
return Model.BALANCED;
}
缓存也分两类:
Prompt 缓存:系统规则、工具说明放前面
结果缓存:城市日销量、文档摘要单独存
最后一定要有限额:
单任务最多 5 次工具调用
最多 2 次重试
最多 8 次模型调用
最长 15 秒
超预算就返回部分结果
最后
Agent 的五个难点可以压缩成:
上下文:维护权威状态
工具:设计强契约
执行:用状态图和预算约束自由度
评估:看分层用例和执行轨迹
成本:并行、缓存、分级和限额
这就像写 Java 服务:真正难的不是调一个方法,而是参数校验、异常处理、幂等、事务、日志、测试和容量控制。
框架会变,这五个问题不会。