前阵子在写自己的agent harness,跑着跑着发现一个问题:上下文越跑越慢。
一开始以为是模型本身的问题,后来排查发现是harness工程上几个坑没填好。
今天把这几个坑写出来,给同样在写LLM应用的同学参考。
坑一:上下文膨胀------把所有历史都塞回去
一开始我写harness的时候,每次调模型都把全部历史对话塞进去。
短任务还好,跑个几十轮之后,上下文长度直接爆。模型处理变长的prompt,延迟指数上升,token成本也翻倍。
后来改了:
1、按消息类型分层------系统提示、用户输入、工具结果、助手回复,分开管理;
2、超过N轮就做摘要------把老的对话压缩成一段summary,而不是原样保留;
3、工具结果默认截断------长文本输出(比如文件内容、搜索结果)只保留前N个token,需要时再取。
代码示意:
python
def trim_context(messages, max_tokens=8000):
while estimate_tokens(messages) > max_tokens:
if len(messages) > 4:
# 把中间几轮压成摘要
summary = summarize(messages[1:-2])
messages = [messages[0], {"role":"user","content":summary}] + messages[-2:]
else:
break
return messages
这个改完,上下文长度稳定了,延迟也降下来。
坑二:工具重试死循环------同一个错误反复调
有一次跑agent,发现它卡在一个工具调用上反复重试十几次。
原因很简单:工具返回错误,harness没有限制重试次数,模型就一直调一直错。
后来加了两层保护:
1、每个工具调用设重试次数上限(我设的3次),超过就把错误信息返回给模型,让它换思路;
2、错误分类------可重试错误(网络超时)和不可重试错误(参数错误)分开处理,后者直接告诉模型「这个调用失败了,换一个方法」。
python
def call_tool_with_retry(name, args, max_retries=3):
for i in range(max_retries):
result = call_tool(name, args)
if not result.is_error:
return result
if is_non_retryable(result.error):
return result
return ToolError(f"工具{name}连续{max_retries}次失败,请换方案")
这个改完,死循环消失了,token成本也省了。
坑三:失败恢复------上下文断了不知道从哪续
跑长任务的时候,harness崩了或者超时了,下次启动不知道从哪续。
一开始我以为是加个checkpoint就行,后来发现没那么简单:
1、每轮对话结束后存一份快照------当前消息列表、工具调用记录、中间状态;
2、启动时检测快照,如果存在就让用户选择「从头跑」还是「从上次断点续」;
3、断点续的时候,把快照里的上下文重新喂给模型,让它知道「我们上次跑到哪了」。
lua
def save_checkpoint(messages, state):
json.dump({"messages": messages, "state": state}, open("checkpoint.json","w"))
def load_checkpoint():
if os.path.exists("checkpoint.json"):
return json.load(open("checkpoint.json"))
return None
工程取舍:自研 vs 现成框架
现在主流的harness路线有几种:
1、纯自研------像我这样,从OpenAI SDK开始自己拼,灵活但坑多;
2、codex式------带工具调用、上下文管理、重试逻辑的现成框架;
3、harness式------更强调状态管理和多agent协作。
自研的好处是你完全知道每一步在干什么,坏处是所有坑都得自己踩。
现成框架好处是坑已经填了,坏处是你得适应它的抽象,有时候想做的事它不支持。
我的建议:如果是学习目的,自研一遍,把这几个坑都踩过;如果是生产环境,直接用成熟框架,别重复造轮子。
harness真正该抽象的是什么
不是把OpenAI SDK拼起来就完了。
harness的核心是:上下文管理、工具调用、错误恢复、状态持久化。
这四件事做好了,模型换成GPT、Claude、还是开源模型,都能跑。
这才是harness该抽象的层。