AI工程重心转移
最一开始,当LLM的输出结果不能达到人们的预期时,人们首先按考虑的是输入的提示词是不是不够准确和全面,如何描述才能让模型听懂我想要什么,这个设计被称之为prompt engineering(提示词工程)。
为了得到更准确的答案,需要不断地补充上下文,prompt中加入越来越多的信息,但是大模型一次可以处理的上下文窗口是有限的,对于时间更久远的对话历史,他可能会忽略甚至遗忘(上下文腐化);与此同时并不是所有的信息都是有用的,大模型并不知道哪些是自己真正需要的,context engineering(上下文工程)就是解决如何让模型知道该用什么信息的。
设计出一个完整的context engineering的确可以保证单步执行可以得到我们想要的结果,但是任务链路一长,还是有可能导致执行结果跑偏,这也是为什么同样使用Claude、gpt这几个大模型,不同段对做的来的agent使用体感却差异很大。所以针对连续行动的情况,需要设计一套准则来约束和监督模型的执行过程,在跑偏的时候可以及时拉回来,就像给马拴上马具来控制前行方向,所以称之为harness engineering(可以直译为马具工程,我觉得也可以称之为环境工程)。
1、prompt engineering
其实平时我们直接看到的界面(不管是chat gpt这类聊天界面还是cursor这种ide),本质上都是一个很大的参数文件,核心就是根据你输入的内容预测结果——输入越模糊输出范围越大,反之越精准。因此人们逐渐摸索出了一套完整的提示词模板:
{yaml:name + resource
角色设定:资深java工程师
背景信息:完成订单系统模块设计
参考资料:相关文档、历史对话
约束条件:不允许修改**文件
输出格式:……}
将可复用的配置沉淀为一个文件,即skill.md
2、context engineering
但是逐渐人们不再满足于简单的对答,而是希望模型可以真实的读取文件并调用工具执行任务;针对具体的执行需求,系统要做的不只是单次回答要正确,还要关注本次需求的完整链路能否打通。
比如我刚刚的需求是写一个订单模块,但实际公司想要得到的是一份基于本公司规范、能够与当前已有项目进行耦合且基于已经通过评审文件的代码,但是他又不能凭空补出你不曾喂给过它的知识,缺少“真实”的资料。如果一次把这些需求加入到提示词模板中,那处理任务对于大模型来说将是相当大的工作量;而且大模型的上下文窗口是有限的,总有容不下你的需求的时候;更重要的是,就算容得下,大模型的注意力也不是均匀分布的,对于更久远的对话,他可能会忽略一部分内容甚至前后矛盾(也就是上下文腐化)。
而context engineering就是解决如何在上下文窗口有限的前提下把最相关的信息告诉模型。核心逻辑可以拆解为三个部分:召回(找到最相关信息)、压缩(保留每一段的核心)、组装(将找回内容按照相关性排序)。
同样是三步走解决了上下文窗口有限的问题,得到相关信息并且同样发送给claude去处理,为什么claude code、cursor、trae这些不同的ide输出的结果还有差异?
这就是另一个需要注意的问题——大模型注意力,这里一个很重要的思路是agent skills提出的渐进式披露:一开始只给模型看到目录,根据目录选出合适的skill再具体展开执行。
3、harness engineering
以上两种方式都更关注输入侧的优化,只能保证模型在单步执行达到我们预期的结果,链路一长仍然可能跑偏,为了及时“悬崖勒马”,需要给系统加一个harness来驾驭它。
关于harness的边界,可以总结为Agent = Model + Harness,在一个agent系统里面,除了模型本身之外,其他所有关系到模型能不能准确交付的部分都属于harness。
换言之,engineering的发展过程相当于逐层套壳,从里(prompt)到外(系统环境)逐层规范化,直到剥离到外层只有大模型。
Harness engineering
Harness engineering最早是由mitchell提出的,从我的视角来看,他作为一个高级古法编程师,他更具有架构思维,或者说从宏观的而角度去看问题。就比如在试验agent的过程中,当agent犯错时,我们一般人会想直接补充一条指令,告诉他不要再犯这个错误,然后go on;但是他会停下来思考能不能直接把这个错误永久修复到环境里,让他从根源上不可再犯。可能这也是当前ai冲击下我们最应该学习的东西,ai可以取代很多东西,写代码的速度和质量都远超刚毕业学生甚至是刚工作几年的工程师(还没工作不清楚),但是整体的架构思维、如何应对甚至预判ai可能会出现的问题仍然是,至少现在还是人类不可替代的原因之一。
一个完整的harness engineering可以划分为三个维度:
输入侧:上下文+记忆管理
动作侧:工具调用+任务编排
校验侧:评估观测+约束
1输入侧
输入主要出从两个维度来考虑,第一个是每一轮发给大模型的上下文要在空间上保持精准且全面,第二个是历史对话怎么持久化用以保证之后对话的正确性。前者就是短期记忆work memory,就是当前对话的上下文窗口;后者long-term memory需要持久化到外部存储(向量数据库、摘要压缩)
短期对话的上下文窗口长度有限,对话轮次一多可能导致上下文溢出,一般可以通过滑动窗口、摘要压缩、向量检索增强(历史对话向量化存入数据库)、关键事实提取(维护一个关键信息表),后面三种都数据持久化到外部。
2动作侧
没有工具的大模型本质还是只能做预测,不能实际操作。MCP通过设计统一接口将工具层标准化,让任何工具都能用同一种方式接到agent上。但是也不能随便接入,什么时候该调用什么工具还是要仔细设计的;调用结果返回给模型之前也要先做提炼排序,否则可能返回太多冗余文本。
接入工具可以让agent完成单步操作,但是如果想要他完成多步连续操作就需要进行任务编排,可以理解为了使装载在货物的火车成功运到终点,需要在中途设立几个站点检查货物情况并指给它下一程的方向,使得火车可以顺利到达终点并交付。
这个过程可以看作一个for循环:思考-行动-再思考-再行动,称作ReAct(Reasoning Action)。
3校验侧
评估阶段看似与任务完成无关,但是却是保证任务完成度最重要的一部分,一个agent能正常运行并出结果并不意味着他的成功率也能达到100%。
具体的评估需要设计一个evaluation set和trace:eval集就是给定一批典型人物并标注好正确答案,后续harness做任何改动都让他把这批任务再跑一遍对比成功率;trace就是记录agent每一次的真实足迹,每一步的决策、调用什么工具、拿到什么返回都是可定位的。
此外还要给定约束,约束agent不可执行的操作并设计失败预案。
实际遇到的问题:
1、agent越跑越偏:
一方面是前面提到的上下文腐化的问题,另一方面也有研究指出agent也会有上下文焦虑,就是当上下文窗口不足时他会自动简化思考步骤和输出,像人一样感觉到自己承担不住这份压力的时候就想要尽快结束。这种情况下仅做上下文压缩是解决不了问题的,因为历史对话的上下文窗口带来的压力还在。
anthropic提出新的解决思路:直接丢掉整个上下文窗口,重新开始。在prompt的设计上做了一些修改,第一轮用初始化prompt,后面每一轮用一个专门的增量prompt推进,然后把新状态写回init prompt,本质是将状态外化到文件系统形成长期记忆。但是还是同一个agent,只是每次打开都是不同的上下文窗口。
2、agent给自己的评分总是偏乐观——生产和验收分离
3、agent运行失败——问题不在于提示词和模型
Openai在开发codex的时候是由agent来写全部的代码,工作重心放在了拆解任务、补充环境中缺失的能力、建立反馈链路。尤其针对第二点,最直接的想法是修改提示词“你不要再出这个bug”,但是openai是给agent接入改造环境,自己写完自己跑自己检查。
4、agents.md越长性能反而越差——抓不住重点/上下文腐化
Openai尝试过将全部规则文件放进agents.md,但是效果不佳,后改成只保留规则的摘要作为核心索引,详细内容存在子文档,只有当真的需要某一部分的时候再去打开子文件(感觉这里和skill的渐进式披露类似)。
5、agent学习到了坏的代码习惯导致越写越乱——将好代码的准则沉入仓库,由agent自己调用去修改代码
Agent = Model + Harness
一个完整的agent有四个核心部分:LLM,memory,tools,action excutor
LLM可以理解为大脑,进行推理分析;memory可以理解为外置大脑,替大脑记住一些规则和历史关键信息;tools是agent与“真实”的接口,代表了agent可调用外部工具的能力;action是真正执行并返回结果的模块。
1传统LLM langchain是定义好每一步的操作按部就班的执行(包含了工作流程workflow和每个节点需要的组件等),流程不会根据每一步的输出而调整;agent是动态决策,每一步都是根据当前节点状态来决定的
2Memory分短期记忆和长期记忆,work memory就是当前对话的上下文窗口;long-term memory需要持久化到外部存储(向量数据库、摘要压缩)
3tools的调用是基于function call完成的,function call让模型可以输出符合特定JSON schema的结构化调用指令,让模型触发工具调用,而不是纯预测(纯猜);
同时在这里还需要用到MCP,MCP可以类比物理世界的USB接口,将不同的tools用统一的协议暴露给不同的模型,使得调用tool的设计复杂度大大减低;
在harness中复杂任务边执行边完善所沉淀出来的agent.md就可以称之为skills,下次遇到类似的任务直接复用。
【三者对比:function call解决模型与函数之间的沟通(单个模型的调用);MCP为工具与LLM的连接提供标准化协议(多个LL和agent之间):skill总结经验以便日后复用】
#Next Loop Engineering