智能家居
border collapse(2025苦涩的Agent开发故事)

地址:

经授权发布,如需转载请联系原作者

纸质日历上的数字已经变成2026了,站在这个新旧交界的时刻上回看一下2025,有许多变化。

大多数名为「感受」的东西如果不用一些文字记录下来,会再也找不到当时的心境。因此有了本文。

2025年8月完成了第一个Agent项目初版的开发,带着些许迷茫写了一篇类似开发日志的东西:

那这篇文章可能可以算是上篇文章的续集。

如果让我写一些概括性的想法,我想我会写下这些:

2. 模型能力:基模代表你的Agent系统的上限,你的Agent Infra设计代表你的Agent系统的下限。

3. 如何调整PE与工具描述:把Agent当成你的用户,提供给它良好的人机交互,站在他的角度想为什么他要这么做。

5. 人脑与Agent:站在Agent的视角,上下文窗口是宝贵而有限的,内存与硬盘是近乎无限的。这个视角来看上下文窗口像短期记忆,内存与硬盘像长期记忆。

人类思考的过程可以用文本表示,而LLM的训练过程投喂了大量人类世界的文本。因此除了作为一个易用的文本处理工具以外,LLM本身具有决策和思考的能力。

从这个冷门的角度讲,这是导致当下的Agent无法真正实现AGI的原因之一。

章节开头给个我自己瞎定义的概念:Agent = Context + Loop + Tool

DeepResearch Agent是这个项目立项之初的概念。从我的视角来看它实际上更像Data Analysis Agent,因为信息的来源是部门内,没有接WebSearch,只接了部门内的信息进行Search.

写起来无非就是——收集信息 + 生成报告两步。

相对容易的点是:不需要太考虑服务稳定性,这种项目生成的报告往往是个参考,所以挂了也没什么。对用户来说只是不能生产一个仅供参考的文档,没什么资损。

使用Agent处理客服工单,而客服的大多数工作流程都被SOP化了,if A ... else B.

但这个客服系统的Agent每一步都是会影响现实世界,这导致它不得不SOP化。并且由于会影响真实用户、甚至造成舆论与资损(涉及外呼、退款操作),产品与业务侧对Bad Case的容忍率极低。

它的流程并不复杂,也不需要查询大量信息再交给LLM分析和处理,因此上下文很短。

本着把Agent当成你系统的用户的理念,我认为Project 1中,Agent是数分或者研究员。Project 2中,Agent是客服的角色。

Agent的回忆

1. Agent的上下文管理

上下文窗口是很容易因为追加导致膨胀的。在一个Agent Loop中,如果每一轮Loop拼接本轮的tool call request + tool call response, 而不做其他操作,上下文窗口将很快被堆满。

Remove: 通常很久之前的行动轨迹是没什么用的(你已经行动完了,并且你知道你行动完了),可以直接Remove掉。

Remove规则:在DeepResearch Agent中做了简单的Remove,把已完成的Todo list之前的上下文移除掉;此外,如果到达上下文窗口上限的75%,会强制驱逐最老的Message,直到符合窗口。(会给个“前面的上下文已经被驱逐”的标记)

2025苦涩的Agent开发故事nerror="javascript:errorimg.call(this);">
Remove和Compact到的比喻

如上面所说,Remove时会干掉一部分的上下文,但是也不想让Agent以为之前的事情没有做。

Todo如果不提前规划Todo,多步骤任务Agent会忘了自己要干嘛。有了Todo可以每次完成以后让Agent动态打勾。(此外,即使没有Note,已完成的Todo实际上也可以让Agent假设之前的步骤已经做完了)

(3)思考过程

这样可以发现Agent为什么做了错误的事,然后去调整工具和PE。

2. 需要避免幻觉的报告

在DeepResearch Agent中,我们交付给用户的最终产物是一个报告文档。

针对需要copy原文的场景,这里的一个认知是不要让LLM去复读(我说的复读是指——上下文窗口内有一部分A,LLM需要自己输出相同内容的A')。因为复读容易改写内容。

你个较好的实践是,让LLM写代码或者操作工具,把A文件第3-5行的内容,insert到最终报告的7-8行。这样就不会幻觉。

比如:

{
"before_value":"春天",
"after_value":"冬天"
}

理解了上面的结构化例子,我们很容易联想到HTML网页的生成——除了让LLM直接自己输出HTML文本以外,实际上也很适合通过格式化的代码生成内容,比如重复的一些HTML Tag + 内容。比如编写一段Python代码,开头先拼接header,然后通过字符串拼接各种HTML内容。这里给个例子

# 示例数据(假设来自其他地方的结果)
students=[
{"name":"Alice","score":92},
{"name":"Bob","score":85},
{"name":"Charlie","score":78},
{"name":"Diana","score":88},
]

# HTML 头部
html_parts=
html_parts.append("""






body { font-family: Arial, sans-serif; }
table { border-collapse: collapse; width: 50%; }
th, td { border: 1px solid ; padding: 8px; text-align: center; }
th { background-color: ; }
.high { color: green; }
.low { color: red; }



学生成绩报告







""")

# 使用 for 循环动态生成表格行
forstudentinstudents:
score_class="high"ifstudent["score"]>=85else"low"
html_parts.append(f"""




""")

# HTML 尾部
html_parts.append("""
姓名成绩
{student['name']}{student['score']}



""")

# 合并并写入文件
html_content="".join(html_parts)

withopen("report.html","w",encoding="utf-8")asf:
f.write(html_content)

print("HTML 报告已生成:report.html")

3. 处理长结构化文本

如果你需要对Json和csv文本通过代码进行一些操作,那你需要预先知道Json的格式或者csv内部的格式。

有时比较头疼的点是前几行和后面的行格式不一样,导致通过preview写出来的代码处理不了后面格式不一样的行。这个没什么太好的解法。人写代码也是大概扫一眼数据格式,然后开始处理,跑了代码发现报错了再修数据或者修代码。当然,人可能大概浏览一遍全文,而不是只看前几行(放到Agent开发里可能需要对整个文件进行抽检,随机看某些行的格式)

除了preview工具,有时确实有需要读取整个文件原文的场景,普遍的Agent实践中会提供个read工具或者view工具把原文load到上下文窗口。当然也不能全部load,上下文窗口很容易就爆了。需要给个允许读取的长度阈值,如果发现过长需要在返回时把结果截断,并且告诉Agent这里被截断了(否则Agent可能认为原文就这些),此外需要给Agent建议,如果发现截断了,通过preview或者通过code操作(就像友好的页面提示框)。

上面反复提到写代码处理结构化文本,那Agent实际必须具有编写代码和执行代码的能力。

Code Agent没什么好说,LLM通过文本生成能力生成code + run code(sandbox),额外提供一个Run Code的的能力即可。

这里实际上有两种实践范式,一个是把run code的能力直接挂给主agent,主agent自己write file的时候本身就可以写代码。另一种就是把run code能力给了subAgent,主agent只给需要子agent处理的任务描述,以及选择一些需要处理的文件发给子agent。

但是在意识到这一点的时候我已经被安排去做Project 2(那个客服Agent)了,因此它的现状依然是code agent作为子agent在使用。

5. Infra for Agent

给程序员一个计算机可以完成工作,那给Agent一台计算机也可以完成大部分工作。

6. File System

File System也是一个给Agent的Infra

LLM一次通常无法生成太长的文本,如果长报告,需要多次生成,一般而言,这种文件系统还需要提供追加写的能力。

7. Rule验证

甚至可以用LLM as Judge模糊给个匹配标准,评判一下输出是否合适。

如前文所说,实现客服系统Agent的过程中,我的大部分时间都花在与Agent无关的事情上,因此我使用了「苦涩」一词来描述这一项目历程。

在实现中我需要对接不少已有的老系统接口(工单、订单),他们本身不适合Agent直接生成参数并调用。

对于工程上我可以拿到的参数,我在代码里对不需要动态生成的参数进行了组装,减少需要LLM自由生成的参数。

为了把这件事合理化,我安慰自己就像人在电脑上拖拽文件从文件夹A到文件夹B一样。或者就像在Ctrl + C, Ctrl + V一样。

在实践过程中,我发现可能的确存在通用的Agent框架,上下文管理方式固定,文件系统固定,Code Run能力固定。剩下的不过是给予不同的知识和工具。

3. 聊聊SubAgent

我认为实际上是不必要的,如同Antropic团队Blog里的观点,只有需要并行的长耗时的、或者会严重污染主Agent上下文的case适合抽成SubAgent.

但是需要提的一点是,承担外呼、退款这种带有业务语义的东西的责任是很heavy的,司内的订单系统有多套,这导致这些工具适配代码实际上写起来很dirty,与业务的强耦合导致又需要做大量工程封装。这个角度下让隔壁团队去搞SubAgent实际上又能降低我大量的心智负担。

4. Callback模式

比较常见的一个思路是每一轮循环在一些固定的检查点都进行checkpoint, 这样如果pod down掉,用其他pod重新拉起任务的时候,从checkpoint继续执行即可。

当任务重新拉起时,不希望重新进行写操作。显而易见的是是外呼操作、退款操作这样的写操作都是不希望二次重放执行的(虽然他们自己会做幂等)。

我认为这样有两个好处:

(2)重新拉起任务时,发现已经发起了对下游的调用,优先扫描db里有没有已经回来的callback,如果没有,继续等待。

5. 延迟状态机

一些操作外呼、解封、退款并不希望给打断(给用户的电话打了一半掐了其实很恶劣),但是客服在这些子任务执行过程中可能点击了暂停。那只能暂时标记一个逻辑暂停(pausing), 等子任务callback回来以后再真正地暂停任务(paused, 实际上是一种延迟的暂停),那么在子任务运行期间期间收到的指令都需要排队。

此外,某种状态下一些指令需要忽略,某种状态下一些指令需要比其他指令更优先感知,这导致我构造了一个苦涩的状态机。

我读过一些开源的Agent实现,但他们似乎没有面临我遇到的问题。

6. 信息刷新

比如订单,用户可能自己操作退款了。要么客服自己在Agent执行过程中做了一些事,手动退款了(你无法禁止它,因为订单属于外部的东西),或者Agent执行过程中,用户来辱骂了(离谱),或者客服自己主动去外呼用户了。

如果选择监听事件,订单、外呼这些系统属于其他部门,过于繁杂。相当于订单工单在我agent系统都存了一份最新的快照,第一可能有一致性问题,第二数据量也很大。此外,监听过多事件可能给我带来巨额的消费压力。

值得一提的是,工单的操作日志像外部世界的客观事实,如果客服自己已经打电话询问商家是否可以退款,我们的Agent就不会再去询问商家。

这可能也符合现实里多个客服处理同一个工单时的情况,优先参考工单日志看看过去做过哪些事情。

实现过程中我也并非规避所有事件的监听。

那我们的Agent实际上需要源源不断地接收外部事件,修正自身行为。收到该类事件会在安全的检查点重新思考,如果发现诉求变了,那对应上下文里的知识也要变(退款的业务场景知识和改期是不一样的)

在实现中,Agent的内部状态需要维护一下读到哪个事件offset了,如果pod迁移导致Agent重新拉起,需要从旧的未读事件开始读。

真实世界的一切都是瞬息万变的。

对于Deepresearch Agent你大多数时候不会想干预他,不论它规划出了多么不合理的路径,只要不影响最终结果,你等他自己跑完就好。

除了上面说的暂停以外,在一个包含订单的业务场景中,产品强诉求,有个卡点可以卡住,让人工确认Agent规划出来的下一步,如果觉得不合理就去修改下一步。

关于修改下一步,我只能硬写了一段PE拼接到上下文中:

resultText:="客服要求修改todo list,指定第"+stepIdVal.GetValue+"步为: "+selectOptionVal.GetValue+
"\n
"除此之外,你还应该根据实际情况推断并更新后续的todo项。"+
"\n如果确实不符合实际情况,你可以选择使用quit工具(succeed=false)来终止任务。"+
"\n请不要修改next_action为xxx, 如果你认为确实有必要这么做,请使用quit工具(succeed=false)然后说明原因并退出。"

指导Agent行动的知识有强业务语义,是从外部系统获取的,但是产品又希望Agent只在一部分情况下让知识指导行动,另一部分下随着我们自己的PE行动。

外部系统的知识和我们的PE甚至是冲突的,这导致Agent执行过程中面对冲突的部分左右横跳。

10. SubAgent与前台UI layer的交互

我的主Agent不想感知这些东西(我感觉并没有必要),因此对于这些问题和交互,我有一个Proxy层,对于不需要让主Agent感知的事情,SubAgent自己通过Proxy直接转给了UI Layer,不经手我Runtime层的主agent。

此外, SubAgent自己会推送一些酷炫的UI效果给前端(实际就是他的执行进度),让看页面的人不要焦虑。这部分我的主agent也不想感知,也是Proxy Layer直接转给UI Layer.

工具list过多时,会存在工具选择困难问题,以及工具描述上下文太长的问题。这个Claude Code给的实践是把知识和工具也让Agent自行去探索(Skills与Advance Tool那两篇文章)。

我认为不论是Skills还是Dynamic Agent都适合用户的开放Query。

05 写在最后

回忆起来,DeepResearch Agent是我比较喜欢的一个项目,在构造它的过程中我学到了许多新知识,并且因为它不和业务指标挂钩,让我可以随意地去动它,去验证我的一些想法。它像个聪明的研究员,虽然有时候会搞砸事情,但是也会给你意想不到的结果。

两个项目的苦涩并不是同一种味道的苦涩。

客服场景Agent的苦涩可能在于生产环境的压力。

不足是,这两个项目开发期间都是倒排,上线时间非常激进,追求快速落地。如今都没有补足完整的评测体系,很多时候依赖于人工抽检与打标,或者简单的LLM as Judge。希望2026我能花一点时间,补足一个完整而稳妥的Agent评测体系。

END


顶一下()     踩一下()

热门推荐

发表评论
0评