把 AI Agent 接进业务系统,我踩了 3 个挺蠢的坑

1 阅读5分钟

一个会写代码的 NPC,白天修 bug,晚上制造 bug。以下是后者的一点副产品。

上个月我把一个 AI Agent 接进了公司的报表流程。需求听着挺朴素:每天早上把昨天的销售数据捞出来,生成一段人话总结,发到群里。

我原以为一个下午能搞定。结果写了两周,中间把一个测试环境的数据库刷爆过一次,还收到一张我至今没敢细看的 Token 账单。

这篇不讲「Agent 三大核心组件」,那种图我画过,画完自己都记不住。就说说我实际踩的三个坑,都挺蠢的,但好像没人在教程里写。

坑一:它不记得自己上一轮干了什么

第一版我写得很直白:把用户的话丢给模型,模型说要调工具,我调完把结果丢回去,结束。

跑起来看着挺好,直到有人问了句「那上周五呢」。

模型非常自信地编了一个数。不是幻觉,是它压根不知道我们在聊上一轮——大模型本身没有会话状态,每一轮对话都是你把完整历史重新发过去。框架帮你做了这件事,但「帮你做了」和「你可以不管」是两回事。

我后来自己维护了一个 messages 列表,每轮把用户问题、模型的工具调用、工具返回的结果都追加进去。听起来是废话,但真写的时候你会发现一个很烦的边界:工具返回的结果可能有几万字符(比如一张全量表),全塞进上下文,两轮之后账单就开始报警。

我的做法是给工具结果做一次摘要压缩再回填。丢掉一点细节,换来能跑十轮不炸,这笔账划算。

messages = [{"role": "system", "content": "你是报表助手,只回答数据相关的问题"}]

def run(user_input):
    messages.append({"role": "user", "content": user_input})
    for _ in range(6):                      # 别让它无限循环
        rsp = llm(messages)
        if not rsp.tool_calls:
            return rsp.content
        for call in rsp.tool_calls:
            raw = call_tool(call.name, call.args)
            brief = summarize(raw)          # 关键:先压缩再回灌
            messages.append({"role": "tool", "content": brief})
    return "这轮没收敛,我先把中间结果给你"

那个 range(6) 是第二个坑留下的疤。

坑二:工具报错的时候,它会认真地再错一次

有天早上群里连着刷了四十多条一模一样的消息。

原因很简单:数据库那个时间段在备份,工具返回了 connection timeout。模型看到错误,判断「那我重试一次」,于是又调一次,又超时,又重试。它特别讲道理,也特别不放弃。

从那之后我给所有 Agent 循环加了三样东西:

  • 最大轮次,硬上限,到数就停;
  • 同类错误熔断,同一个工具连续失败 2 次直接跳出,把错误原文交回给人;
  • 错误要写给模型看,不是抛个 Exception: failed,而是「数据库连接超时,可能是每日备份窗口,请停止重试并告知用户」。

第三点最反直觉。我们习惯把错误藏在日志里,但 Agent 是拿这段文字做下一步决策的,你给它一句废话,它就只能猜。

熔断那块代码我写得很土,就是个 Counter,但它救过我至少两次。

坑三:账单不是按「次数」算的,是按你有多啰嗦算的

上线第一周,日均两百来次调用,我以为花不了多少。月底一看,比我预估高了四倍。

拆开看,八成消耗在那份被我反复塞进上下文的系统提示词和工具 schema 上——每一轮、每一次工具调用都带着它们重新发一遍。多轮循环意味着多次模型调用,这个乘法很多人(包括当时的我)在写 demo 时完全没感觉。

后来做的几件事,按收益排序:

  1. 能一轮出结果的任务,别硬套 ReAct 循环;
  2. 工具描述砍到只留必要参数,schema 里那些「为了可读性」的示例全删;
  3. 结果缓存,同样的数据别问第二次;
  4. 便宜模型能干的分类/抽取,别让旗舰模型上。

第四条实施完,成本大概降了一半,效果没变化。有点尴尬,但确实如此。

所以 Agent 到底是什么

我现在的答案比较没出息:就是一个会自己决定下一步调哪个函数的循环,加上一堆防止它闯祸的护栏。

「规划」「记忆」「反思」这些词都对,但真正在代码里占篇幅的,是超时、重试上限、上下文裁剪、错误文案、成本控制这些一点都不性感的东西。教程里那张漂亮的架构图,落到仓库里大概 20% 是模型调用,80% 是这些边框情况。

也没必要被框架选择逼死。我这个项目就两个工具、流程固定,最后是自己写了不到两百行编排代码,没上 LangGraph。工具多起来、流程会分叉的时候再换,那时候你才知道自己到底需要什么。

顺带说一句,那个报表助手现在还在跑,每天早上八点准时冒泡。偶尔数据库备份窗口会崩,然后它会老老实实说「连接超时,我停了」——比第一版那个连刷四十条的愣头青顺眼多了。


冒泡NPC,偶尔冒泡,讲点你大概用不上的专业冷知识。