做了近两年的Agent开发,其实真正要学的就是这五件事

这两年一直致力于公司的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 最近几轮对话,用来理解"那杭州呢"

一句话:程序维护状态,模型解析意图。

落成流程就是四步:

  1. 模型读取最近对话 + 当前状态,输出结构化意图
  2. 程序校验城市、日期、指标是否合法
  3. 只合并允许变化的字段
  4. 用新状态去查数据 对应 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,把魔都解析成上海"
);

这样程序知道能不能重试,模型知道下一步该做什么。

工具层还要做三件事:

  1. 参数校验:城市、日期、范围在工具层强制检查
  2. 幂等:下单、发消息、删除数据必须带幂等键
  3. 权限分级:只读、可逆写、不可逆写分开处理

一句话:不要指望 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 和费用

一句话:优化成本前先看路径,不要只看单次模型调用。

实际落地可以按五步做。

  1. Trace 记录每步耗时和 token
  2. 无依赖调用并行
  3. 按任务选择模型
  4. 能缓存的先缓存
  5. 设置硬预算 模型路由不用复杂:
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 服务:真正难的不是调一个方法,而是参数校验、异常处理、幂等、事务、日志、测试和容量控制。

框架会变,这五个问题不会。

相关推荐
七牛云行业应用3 小时前
Qwen-Image-2.1 开源部署完整指南:Diffusers、ComfyUI 与推理服务
ai编程
晨米酱3 小时前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
invicinble3 小时前
记录一个学习技术栈的想法和思路
agent
染指11104 小时前
122.Agent-LangChain核心组件-中间件-动态提示词(dynamic_promapt)
人工智能·langchain·agent·agents
福如意如我心意4 小时前
TencentDB Agent Memory和其他开源memory
ai编程
是Dream呀4 小时前
中秋国庆回家不背电脑,用ToDesk远程反连学校设备,查资料、改作业
人工智能·agent·todesk
漂着的圆木5 小时前
Agent 功能参与度:Copilot 怎么算
sql·数据分析·agent·githubcopilot·度量
程序猿编码5 小时前
告别改源码适配模型:纯 C++ 可配置 LLM 推理引擎,全格式全结构兼容
c++·大模型·llm·推理引擎
小虎AI生活5 小时前
腾讯开源了一个项目,让 AI 直接用你已经登录好的浏览器
aigc·ai编程