前言 @
我开发nekocode项目已经一个多月了,这个项目完全是vibe出来的,我没写过一行代码,但不代表我对于该项目的方案以及细节一无所知,该项目目前已经超过两万六千行代码,在没有财力和条件使用国外claude或gpt等先进模型,仅使用国内便宜模型的情况下,即使是claude-code或者codex这种工业级的codeagent想完美的实现超过万行代码的项目也是十分困难的,最关键的还是人对于方案的把控和项目发展的约束。
vibecoding反思 @
即使是目前十分成熟的agent工程也仅仅是约束llm的行为,让其更加准确,但无法强化llm的底层能力,也就是说相同的codeagent配合不同的模型,达到的效果肯定是不同的。目前比较流行harness这个词,其实可以将llm比作马,而harness就是好的马鞍和缰绳,俗话说"好马配好鞍",但鞍再好,马不行那也白搭。
最近两个月我大概花费了超过100亿的token, 主要使用的模型服务有deepseek、mimo、glm等国内模型,使用的codeagent则主要是codex、claude-code、opencode,也浅尝即止过reasonix、crush、oh-my-pi, 但综合来说前者依然是最好的"鞍具"。
使用下来的体感是codex综合能力较强,前端ui、代码审查、技术改造都可以hold住,但token花费较高,可能是codex默认开了thinking模式,相比来说claude-code代码能力很稳健,不会过于激进,也更省token。还有一个据说缓存命中能达到99%的reasonix, 虽然极致省token, 但感觉整体功能并不完善。oh-my-pi则是在某些方面出众,但综合使用体验依然比不上claude-code,所以我还是更推荐使用claude-code或者codex进行代码开发。
NecoCode现状 @
谈一谈necocode的现状吧,这个项目最初是我为了深入理解agent工程所学习的练手项目,但如今整体框架已经十分完善,无论是基础使用或者用它学习agent的架构思维,都是有一定的帮助,所以下面内容聊一聊核心模块开发思路。
Agentloop @
开发agent首先必须要设计agentloop, 目前已经有非常多的设计思路,包括ReAct、Plan-and-Execute、Reflexion、ToT等等,但目前主流codeagent底层几乎都是标准的ReAct循环,nekocode也是一样。
一轮循环主要流程就是用户提出问题,llm进行推理思考、是否要调用工具(function calling),如果需要调用工具,则循环继续,llm根据工具返回结果判断是否要继续循环,否则退出循环,然后回答或总结用户问题。
不过有些codeagent会设计maxsteps, 主要还是防止模型进入死循环,不过死循环的预防方案很多,除了maxsteps, 还有hook机制或者简单的if代码强制干预。
SubAgent @
SubAgent是个很复杂的东西,很多人认为多agent运行不是可以更快执行任务,或者执行更复杂的任务,但实际目前所有的主流subagent的实现都没有想象的那样高效率,例如我提出几个问题:
- 主Agent的上下文应该如何传递给Subagent?fork还是完全不需要传递?
- 主Agent与子Agent之间如何共享信息?
- 多个子Agent之间如何协作?如何共享信息?
这不仅仅是工程问题,也是预算问题,众所周知目前很多llm厂商都会有前缀缓存,缓存命中则大大减少推理量,也更省钱,因此所有的设计必须在能力与预算之间衡量取舍。
什么是前缀缓存?简单说上下文是由多个消息组成的,包括系统消息、用户消息、LLM回复消息、工具调用消息,基本上agent每次回复都会携带所有的上下文,例如第一轮的消息组成:A+B+C, 第二轮的消息组成A+B+C+D, 对于有前缀缓存优化后,第二轮的A+B+C则命中缓存,使用更低的token价格收费,只有D才用标准的token价格收费。如果第三轮擅自在上下文中穿插消息E, 比如A+E+B+C+D, 则只有A命中缓存,E+B+C+D缓存失效,导致花费更高的token价格。
claude-code也有两种subagent的上下文传递方案,一种是完全fork主agent, 也就是继承完整上下文。第二种则是独立于主agent之外,不继承任何上下文,但前者几乎很少使用,往往是后者使用较多。至于子agent与子agent之间的协作,目前来看几乎不需要实现,因为成本较大,复杂度也高。
这也是预算方面的取舍,通常的方案是主Agent委派subagent去执行一个独立于主agent上下文之外的任务,然后返回结果给主agent。这样的好处有几点,一个是委派复杂的任务不会污染主agent的上下文,节省上下文窗口,第二点是预算方面更加节省。
因此nekocode干脆只实现了这种纯净的subagent委派,通过一个task的tool委派任务,接入subagent的agentloop循环,当subagent完成任务后,将信息作为tool的结果返回给主agent,当前几乎所有codeagent都是如此设计的。
上下文管理 @
上一节提过,很多llm会有前缀缓存的优化方案,因此上下文管理的设计优先需要保证的就是如何最大限度的利用前缀缓存?比如你的系统提示词、对话消息、用户偏好、工具信息、skill信息、项目规范这些内容如何组织?如果组织的有问题,在后续对话过程中发生结构的紊乱,那很大程度上你的上下文会发生大量的缓存失效,token成本激增。
因此一般上下文是分层设计的,哪些信息是基本不动的,哪些信息是可能变化的?尽可能将稳定的内容作为前缀。
Nekocode就是分层设计的,Layer0层用来存放system prompt、skill、tools、项目规范信息,Layer1层负责存放摘要压缩信息,Layer2层负责存放对话信息,Layer3层则存放hints信息。(至于hints是什么,后续会聊到)
上下文的分层设计基本就是这样,所有的codeagent都是如此设计的,更重要的则是上下文压缩的方案。大部分llm厂商的上下文空间是有限的,随着对话任务越来越多,上下文空间全部塞满后,对话就无法再继续了,因此必须要压缩处理。
压缩有很多方案,例如claude-code的方案是多层流水线:
-
对工具调用信息进入上下文之前进行裁剪,比如grep的信息只保留尾部几行,或者bash命令的大量数据裁剪截断,目前有个开源很火的项目headroom也是通过事先压缩处理来节省上下文窗口以及token花费。
-
工具输出压缩,最简单的tools snip技术,从最老的消息开始,将工具输出的结果置为一个简短的字符串,因为很多过时的工具输出信息已经没有参考价值,但还占着上下文的位置,不过缺点是如果改动了消息内容,会导致历史消息全部缓存失效,对此claude的解决方案是做了特殊处理,使用cache_edits字段从模型底层避免缓存失效。
-
对话总结机制,这也是一种微型的摘要压缩,针对用户最近的几轮消息进行摘要压缩,然后取代这部分轮次的消息,来节省上下文空间,所以你可以看到claude-code在对话结束后过一小段时间底部会生成一份小的总结摘要。
-
当上下文空间非常有限后,你可能想到从最老的消息开始删除,但这会触发大规模的缓存失效,因此不如全量的对话压缩,当然也有两种模式,一种是基于上一条的多轮对话总结,尽可能保留多的上下文,如果还不行,则触发全量的上下文摘要压缩,然后开新分支进行对话,这就是最后的兜底手段,也就是compact命令的思路。
上述简单谈一谈claude-code的压缩,还有一种极简压缩方式,就是reasonix的压缩,reasonix为了追求极高的缓存命中,干脆不对历史消息进行裁剪或重组,只要上下文窗口满了,就开启全量摘要压缩并且另起一个新分支进行对话,那相对来讲也不需要设计这么复杂的压缩流水线机制,但最终保留的上下文质量效果,肯定是不如claude-code的,因此nekocode还是选择和claude-code一样的设计思路来进行压缩。
ToolUse @
好马配好鞍,这句话一点都不假,Tool的设计在整个agent是十分重要的。例如模型如何读写文件、如何搜索内容、如何执行命令?这中间也有很多工程化的问题,一个codeagent产品首先需要实现的就是read、write、edit、glob、grep这几个工具,基于这些工具,agent才能感知代码、感知项目。
每个工具都有很多设计思路,比如read怎么读?怎么传参?模型最多读几行,write怎么写?edit如何编辑,replace还是别的机制?怎么确保模型进行稳定的edit?即使是claude-code,如果模型的指令遵循度不高,实际使用过程中可能会传错参或者乱传参,导致edit失败,你可能也会发现claude-code经常Update失败,然后被迫开始各种sed命令来完成文件修改。
除此之外,还有一些辅助工具,例如task用来委派subagent,websearch来联网搜索,webfetch来获取网页内容、例如codex还加入了image_gen来生成图片等等,不过重点我想聊一下edit的实现。
claude-code是基于replace来实现文件修改的,因为这个方案最简单,但replace也不能无脑替换,代码有很多相似的部分,因此必须锚定行号范围,所以基于行号范围的replace成为了最简单的edit,但其往往不够稳定,比如每次edit后文件内容和行号都会发生变化,如果不再进行一次read,那么下次edit大概率会失败。所以oh-my-pi提出了另一种较为复杂的方案,就是hashline方案,通过计算filehash来确保文件的一致性,具体可以看看代码,在此不多赘述。
辅助系统 @
上面谈到的是我认为比较核心的东西,但为了提高agent的能力,一般还会有skill、mcp、hook、plugin等等系统。skill、plugin、mcp我想大家基本都很熟悉了,其工作原理,网上很多讲解教程,因此在这里重点讲下hook机制是如何约束llm行为的。
你可能偶尔会遇到agent进入死循环,例如重复的工具调用,大量的只读不写操作,hook就是在模型推理前后,工具调用前后代码注入特定的提示词来避免agent进入死循环,或者约束模型行为的系统,claude-code就是这么设计的,其他agent也是这么设计的,如果你看过知名的superpower或者gstack等harness的skill插件,你就知道他们也设计了大量的hook注入内容。
所以necocode也实现了这些基本的辅助系统,有些codeagent还会为每个编程语言引入lsp来辅助编写代码,但这太重了,因此necocode选择的方案是仅仅使用tree-sitter就够了。
github有个知名的codegraph项目就是基于tree-sitter进行项目的静态解析,这样会让agent进行更少的read或grep调用,快速理解代码语义以及定位代码位置,当然这些辅助系统在整个agent工程中也是非常重要的。
结尾 @
目前necocode仍然处于试验阶段,有很多仍然未解决的幻觉问题,能力也比不过claude-code或者codex这种成熟的工业级产品,但其整体架构已经非常完善,缺少的更多是harness的工程约束。不过claude-code也存在一样的问题,例如我让其分析项目bug, 往往第一轮误报会很多,第二轮让其复盘后幻觉大量减少,因此我认为二次思考是目前解决幻觉很有效的方式,第三次则效果不明显了。
解决幻觉有很多的优化方案,比如注入当前时间、注入项目技术栈、版本号等等,也能大幅度降低幻觉,总之我认为necocode目前也值得一玩,你可以在我的github下载完整的 necocode二进制,欢迎各位学习使用,如果有bug或者更高的设计思路也欢迎各位提交、反馈!