全栈是逼出来的,不是选的。
这两年一直致力于公司的Agent开发。总结了一些思考和解决方案,今天给大家分享下,也是给自己的一个复盘。
做 Agent 最有意思的是,通常是 Demo 刚跑通的那一下:模型理解了需求,自己选了工具,还给出了看起来不错的结果。
真正让人头疼的是,是第二个阶段。
用户换了一种问法,状态丢了;工具返回一个含糊的错误,Agent 开始乱试;你改了提示词,一个场景好了,另一个场景坏了;上线第二天,账单和延迟一起超预期。
所以,Agent Demo 和 Agent 系统之间,隔着一层工程问题。这就是通常所说的工程化解决方案
这层问题不是换框架能解决的。框架解决的是调用模型、注册工具、组织消息这些胶水层;真正难的是下面五件事:
状态怎么传?
工具怎么约束?
错误怎么恢复?
效果怎么回归?
延迟和成本怎么控?
LangChain、LangGraph、Spring AI、AgentScope 或自研循环,这些都是我们去做一个agent的基本框架,工程化的问题,他们依然存在。
可能这里有人嘴犟了,推出市面上一些比较好的开源框架 比如字节的deerflow,这个我们公司也在用,的确把一些基本上工程问题化解掉了,但就像你做springboot、cloud开发一样。你要知道怎么解决实际业务问题,实际的业务开发才是硬道理。
今天这篇文章带你从难点展开。每一节都先讲麻烦在哪里,再给可落地的解法。不管你是否搞过agent开发,都能明白。(多说一句,不用在乎什么语言,不管是Python、亦或是Spring ai生态,其实都是一致的,我们需要掌握的是解决思路与方案)
一、上下文:难在状态会丢
首当其冲肯定是上下文,上下文的难点:不是写 Prompt;
假设我们要做一个“汽车门店销量助手”。
用户先问:“帮我看看上海上周的销量。”
下一句又问:“那杭州呢?”
人一听就懂:用户要查“杭州上周的销量”,继承“上周”和“销量”,只把“上海”换成“杭州”。
但如果系统只把最后一句“那杭州呢?”传给模型,模型就不知道该查销量,更不知道时间范围是上周。
很多人的第一反应是把更多资料塞给模型。这样做短期有效,长期会引入三个问题:
状态丢失:用户说“换成上周”“再看另一个对象”,关键信息在历史对话里,不在当前输入里。
状态冲突:系统提示说只能查最近 30 天,历史里还有用户之前查过的去年数据。
重点稀释:工具返回、检索片段、执行日志全部塞进上下文,模型反而找不到关键约束。
Agent 的上下文不是一个聊天记录列表,而是一个随任务推进不断变化的运行时状态。
正确做法类似 Java 里的领域对象:维护一个明确的查询状态。
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";
}
用户说“那杭州呢”,只更新一个字段:
state.setCity("杭州");
不要把整个对象重建,也不要让模型随意覆盖所有字段。
一个实用的上下文结构是:
State 当前任务状态:城市、时间、指标
Evidence 本次回答需要的证据:销量数据、报表片段
History 最近几轮对话,用来理解“那杭州呢”
一句话:程序维护状态,模型解析意图。
落成流程就是四步:
模型读取最近对话 + 当前状态,输出结构化意图
程序校验城市、日期、指标是否合法
只合并允许变化的字段
用新状态去查数据 对应 Java 大概是:
QueryIntent intent = llm.parseIntent(history, state, "那杭州呢?");
if (!cityRepository.exists(intent.city())) {
throw new InvalidCityException(intent.city());
}
state.apply(intent); // 只改 city,保留时间和指标
二、工具:难在把概率系统接到确定性接口上
工具调用看起来只是让模型调用函数,麻烦在失败场景。
比如模型调用:
querySales("魔都", lastWeek);
系统可能遇到:
魔都不是标准城市名
日期算错
查询结果为空
接口超时
用户没有权限
如果工具只返回字符串:
String querySales(String city, DateRange range);
模型分不清空结果、参数错、超时和权限问题,只能靠猜。
比如可以像设计 Java 接口一样设计工具契约:
record ToolResult(
Status status,
Map<String, Object> data,
String message,
boolean retryable,
String nextAction
) {}
enum Status {
SUCCESS, EMPTY, INVALID_ARGUMENT,
PERMISSION_DENIED, TIMEOUT
}
城市名错误时返回:
new ToolResult(
INVALID_ARGUMENT,
Map.of(),
"city 不是标准城市名",
true,
"调用 resolveCity,把魔都解析成上海"
);
这样程序知道能不能重试,模型知道下一步该做什么。
工具层还要做三件事:
参数校验:城市、日期、范围在工具层强制检查
幂等:下单、发消息、删除数据必须带幂等键
权限分级:只读、可逆写、不可逆写分开处理
一句话:不要指望 Prompt 保证安全,接口必须能挡住错误。
写工具时可以直接套这个模板:
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 状态机:
if (state == RESOLVE_CITY && cityValid) {
state = FETCH_SALES;
}
再加上预算:
最多 2 次销量查询 最多 1 次城市解析 最多 1 次重试 总耗时不超过 10 秒 最后由程序验收:
boolean acceptable =
result.contains("上海")
&& result.contains("杭州")
&& result.timeRangeIs(lastWeek)
&& result.referencesToolData();
不要相信“模型说完成”。
一句话:模型负责决策建议,程序负责边界和验收。
可以直接落成三个对象。
第一,节点定义:
enum Node {
CLARIFY_INTENT,
RESOLVE_CITY,
FETCH_SALES,
ANALYZE,
GENERATE_RESULT,
CONFIRM_RESULT
}
第二,预算控制:
class TaskBudget {
private int llmCalls;
private int toolCalls;
private int retries;
boolean allowToolCall() {
return toolCalls < 5;
}
boolean allowRetry() {
return retries < 2;
}
}
第三,验收器:
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%。
这个结论可能错在四层:
意图层:用户问门店销量,Agent 查了城市总量
工具层:只查了杭州,没查上海
数据层:把上周日期算错
生成层:数据是 8%,写成 18%
只看最终答案,无法定位问题。
所以要分层评估:
[
{
"id": "followup-city",
"history": ["帮我