Loading...
Loading...
前段时间,看到实验室的同学在学LangChain——一个Agent框架,我也来了兴趣,去了解了一下Agent的实现和LangChain的用途,并开始构思如何将Agent这一前沿技术放到我的项目中,做一次工程实践落地,增强我对这个新概念的理解。
我对LangChain的认知就是一个封装了很多 Agent 接口的框架,更准确地说,它像一个介于LLM和Agent之间的胶水层,将各种东西组合起来,可以快速实现一个 Agent 产品。
Agent是最近随着AI产品应用领域的发展而兴起的一个新概念,中文名叫智能体,简单来说,就是将AI从“只会说不会做”的限制中拉了出来,赋予了它决策、计划、记忆和执行的能力,让AI不止于文字的说教,而是真的能落地一套可行的解决方案。
在我看来,一个完整的Agent产品至少应该具有制定计划、记忆检索、思考决策、工具调用和检查复核这五大核心功能,而现阶段的很多Agent产品的实现思路也主要就是围绕着这几大功能点展开的。
业界针对这几个功能提出的解决方案有很多,不过现在主流的方案也就是那么几个——Prompt、MCP、Skill、Tool Calling、RAG、ReAct、A2A等。
但是,业内出现的方案越多,想要实现一个Agent的难度也就越大,因为这些功能并不是一套很完整的可复用产品,更像是指导了一个方向,而具体的功能实现,还是要靠开发者自己。
举个例子,如果我需要给LLM实现RAG功能,那我就需要从头做出一整套RAG的实现方案,先提取 embedding,再在上下文中进行检索,然后将检索到的高相关度文档片段与用户查询拼接成增强型 prompt,送入大模型生成响应;同时还需配套完成文档分块、向量库构建、结果去重与幻觉校验等环节,最终形成一个从数据预处理到回答生成的完整流水线。
你可能想说,为什么我们不能复用成熟的RAG工具,而非要从头实现?
我的答案是,从更宏观的角度来说,工具和工具链始终还是有区别的,真正实现Agent产品的是一套将各个功能串连起来的工具链,而不是一个个各自为营的工具,每个工具各自的接口和标准都不一样,想要让他们像齿轮一样完美契合在一起工作,也不是很容易。
所以很显然,完全从头实现一套Agent方案的成本有点高。
而更加致命的一点是,我们一开始的目的仅仅是想实现一个Agent产品而已,却在前期耗费了非常多的时间在基建上。并且每多做一个Agent产品,就需要多一次从头再来的步骤,这个成本实在是有点太过于高昂了。
那么有没有一个东西,它可以帮开发者准备好一个Agent产品所需要的大部分功能,让我们能够更加专注在Agent产品的应用层上面,而无需顾虑底层的能力实现呢?
那自然是有的。LangChain 正是为解决这类“重复造轮子”和“标准不统一”的困境而生的。
说回LangChain,它的核心价值就在于把 Agent 所需的计划、记忆、检索、工具调用等底层能力全部封装成了可直接调用的接口,还抹平了不同 LLM 之间的协议差异,提出了一套统一化的标准。你不用再从零搭建 embedding pipeline、向量库对接逻辑或是工具调用兼容层,只需像搭积木一样,把 LLM、记忆组件、RAG 模块、工具集按需组合起来。
比如要给 Agent 加上下文记忆,几行引入代码就能实现;想接入知识库检索,直接绑定文档库与向量库即可。这种高度封装的设计,让我们能彻底从繁琐的基建中抽离,真正专注于 Agent 的业务逻辑与交互体验,大幅降低了 Agent 产品的开发成本与周期。
在简单的了解了这个框架过后,我也来了点兴趣,想要用它做点东西。
于是我回顾了一下我现有的项目——主要是协同画板(Aevia)和依赖分析工具(Deplens),前者似乎不太适合融合Agent功能,毕竟它的定位是一个灵感创作和协同交流的产品,Agent没有什么用武之地。
而Deplens作为一个工程化的依赖分析工具,它无疑是比较合适的选择,因为我认为它缺少一套系统化的流程来实现完整的依赖分析,它现在更像是一个依赖检查工具,而不是依赖分析工具。
检查和分析,两个字的区别,实际上的定位却天差地别。“检查”只是将依赖的引用情况扫一遍并统计出来,简化了人为检索的这一过程;但“分析”是能够具象化的判定引用关系、评估操作风险、制定优化计划等。透过现象看本质,这才是作为一个分析工具应该要做的事,而不是止步于静态的收集、统计和检查。
因此,我开始了对Deplens进行大刀阔斧的改造。
从结果上看,这次改造并不是简单地“给一个CLI工具接上大模型”,也不是让它成为一个华而不实的AI聊天壳子,更像是重新审视了一遍Deplens的能力边界,然后去思考:Agent究竟能起到什么作用,它到底应该放在这个系统的哪一层,才能真正起到增强作用。
最开始的时候,我也因为一些刻板印象而掉进了思维定势。一开始想到Agent,我很容易本能地把它定位成“自然语言交互 + 工具调用”,也就是用户提一个问题,模型去调用几个工具,然后给出一个答案。
这个方向当然没错,但如果只停留在这一步,它本质上还是一个“会说话的查询器”,只是把原本命令式的调用入口换成了聊天框而已,真正的分析能力并没有发生质变。
而Deplens的核心问题恰恰不在于“用户不会问”,而在于传统静态分析本身存在盲区,这是我在后续的思考过程中认识到的一点。
对于依赖分析这类问题,纯静态分析能很好处理大部分的标准场景,例如标准的import / require引入、package.json中的依赖声明、monorepo workspace的包级边界等,这些内容显然都适合用 AST、依赖图和规则系统去做,因为它们本身是确定性的、可枚举的、可验证的,天然能被静态分析。
但问题在于,真实项目里的依赖使用方式远不止这些。比如在实际测试中,我就遇到过一些“非标准”的情况。
例如,在一些特殊场景中,import可能并不一定是静态引入的,它可以通过字符串变量组合的形式动态拼接出一个依赖名并引入。这种动态引入类型在大半个工程化领域中都是一大难题——因为静态分析工具不可能真实的运行代码,并得到一些只有运行时才能拿到的计算结果。Deplens也是同理,尽管在之前,我通过引入Terser和Babel这两大优化工具,尽最大可能缩小动态引入场景的数量,但想要完全排除这类问题的影响显然是不可能的。
又比如,有些具有插件系统的依赖,例如Babel,它们本身可能提供了一些自定义的接口来实现插件引入,这些插件本身可能也是一个依赖,但并没有在项目中通过标准的引入语句被引用,这也就导致了Deplens因为无法正常分析这些自定义的接口而产生误报。
再者,有一些插件,它们本身不会被项目代码明确引用,但是会出现在一些自定义文件(如tailwind、eslint等)、package.json的script属性(如nodemon、tsc等)中,如果Deplens只分析项目代码,就查不到这些特殊的引用方式。但市面上的各类前端工具链这么多,迭代速度又如此之快,我要是一个个的适配,永远都适配不完,Deplens的分析结果也永远都不可靠。
综上所述,这些场景有一个共同点:工具很难只靠传统静态分析给出一个完全可靠的二元判断,即它到底有没有被使用。
因此,在学习完Agent的基本思想后,我的脑中产生了一个大胆的想法——能不能利用AI本身具有推理能力和思考能力的特性,通过引入AI复核机制,对于上述这些情况进行二次复查,让AI根据分析结果和代码上下文,推导和分析这个插件是否有引入呢?
这个想法一开始听起来其实是有点激进的,因为它意味着我要重新定义 AI 在这个系统中的定位。
在传统认知里,AI更像是一个“问答层”,也就是系统把已有的数据和接口暴露出来,用户提问,AI 去做一次自然语言层面的组织和总结,最后把结果解释给用户听。这个过程当然有价值,尤其是在Deplens不久前新加入的review模式(一个能够在分析出依赖引用关系的结果后,将分析过程和结果交给AI,让用户能够用自然语言与它交互,在聊天中获得更加合理的分析、建议和计划的模式)下,它能显著降低用户理解项目依赖关系的门槛。但如果只是停留在这一层,AI更像是一个解释器,而不是一个真正参与分析过程的模块。
而我真正想做的,是让它从“解释结果”更进一步,进入“参与判定”的阶段。
当然,这里的“参与判定”并不是说让AI替代原来的静态分析逻辑。恰恰相反,我一直觉得像依赖分析这种事情,最核心、最底层、最确定性的部分,最终还是应该交给规则系统和静态分析去做。
例如传统的标准引入语法、package.json中的依赖声明、Lockfile中的隐式依赖关系,这些内容都天然适合用AST、依赖图、索引和静态规则来解决,因为它们本身是可枚举、可验证、可复现的。在静态分析中,相同的输入无论执行多少次总是会获得相同的输出,非常稳定。如果把这些内容交给AI,不仅不会更准,反而会让系统的确定性下降。
所以后来我明确的一个重点是,AI不应该替代Deplens原有的分析主干,它更适合放在那些规则系统最难覆盖、但又真实存在工程价值的边角区域里,作为一个“增强层”去补强静态分析的盲区。
要做到这一点,首先就不能直接把 AI 丢进系统里让它自由发挥,而是要先把 Deplens 自己的分析结果进一步结构化,把那些原本隐含在逻辑里的判断依据,显式整理出来,变成 AI 可以消费的“证据”。用强逻辑性的证据链、因果关系和代码上下文将AI的思考范围完全限定在我们预设好的范围内,抑制AI幻觉和思维发散的情况,让它根据已有的内容进行分析,而不是对模糊不清的边界场景乱猜测。
于是,我做的第一件事,就是在原有分析流程之上补了一层 evidence,我也将它称为证据链。
就像警察办案和逮捕需要一套非常完整的动机和证据链一样,判定一个依赖是否被使用,为什么被使用,也应该有一套完整详细的证据链。
在之前,这条证据链是被潜藏在分析环节中的,是一个依靠代码语法组织起来的,约定俗成的规则,但现在我要做的就是将它打捞出来,具象化成一个有实体、可追溯、可推导的,真正的逻辑证据链。
它不再只告诉系统“某个依赖未使用”,而是把这个结论背后的支撑条件显式拆出来,比如它在哪里声明、它是否存在标准引用、它为什么会被判成某种结果、它是否出现过一些可疑但不够强的使用线索等等。
从工程设计上看,这其实是在把原本“藏在分析器里的推理过程”外显出来。这样做的好处也很明显:后面不管是传统的 CLI、review 模式、LangChain tools,还是上文提到的AI二次复核,它们拿到的都不再是一个生硬的结论,而是一组结构化的证据对象。
同时,我还在分析中加入了一个新的行为——统计出那些疑似使用了某个依赖,但在静态分析中无法完全下定结论的可疑片段,例如出现了某个依赖名的完整字面量、项目目录中有类似*.config.*之类的文件,或者是package.json的script属性中出现了某些不属于npm和pnpm工具链的关键词等等,这些都会被收集起来,作为“候选人”(Candidates)等待后续的AI复核。
说了这么久,终于轮到我们今天的主角——AI二次复核登场了。
首先回答:这是什么?
我对它的定位就是一个基于AI打造的、增强型的二次review环节,专门为了解决我们先前提到的动态引入和非标准语法引入两种场景中依赖分析结果不准确的问题。
在复核阶段,系统会将分析阶段产生的可疑引入片段丢给AI,并通过预设各种工具接口,为AI提供了诸如查找证据链、读取代码上下文等能力,让它在搜集信息、思考、决策的流程中,推理出某个依赖到底有没有被引入。
当然,出于保守性的考虑,即考虑到AI的分析结果不一定准确,以及一些实在是“过于动态”的场景(比如完全依赖用户操作和后端数据决定最终结果)真的无法被静态分析,我也没有将复核过程设计第二个静态分析系统,粗暴的将一个依赖划分为“已使用”和“未使用”两种极端的状态,而是通过一系列综合的分析,最终得出一个置信度,并根据不同的置信度,给用户提供不同的行动建议,而不是直接跟用户说:“这个依赖没用,把它删了吧”,这种结果既不负责任,也非常不安全。
关于置信度的计算,它比较复杂,简单来说就是根据可疑片段的类型(字面量、配置文件、script属性等),以及AI对它的分析结果,综合赋予置信度,例如只在字面量中出现过的依赖名,显然就远比具有配置文件的依赖要不可信的多,所以字面量类型的基础置信度就会比配置文件类型的低一些,后续通过AI复核,再对它进行一轮判定,最终决定它到底是什么性质的问题,并采取不同的方案。
接着,让AI分析哪些依赖也是一个问题——复核这个环节本身是一个不小的工作,它需要收集大量的信息来证明某个依赖到底是什么状态。小项目还好,代码量和证据链都不是很大,但如果是在一些商业化的大型项目中,如果对全部依赖都进行一次复核,消耗的Token将会非常恐怖,并且工具分析的耗时也会大幅增长,因此,出于这些原因的考虑,我最终选择让AI只分析那些在静态分析环节中被判定为未使用的依赖,因为我们最终的目的是为了找出未使用的依赖,Deplens这个工具诞生的最初目的也是为了减少误报率,这里的误报指的是“误报成未使用”而不是“误报成已使用”,因为误报成未使用的风险和影响往往比误报成已使用的大得多,所以最后我做出了这个选择,并且将AI复核这个过程做成了一个可自由开关的选项,避免频繁浪费Token。
最终,经过一段时间的开发,我最终实现了这一功能,结果如下:

在这个例子中,我让Deplens分析了一下它自己,可以看到,工具并没有将静态分析和二次复核的结果混在一起,而是分成了两部分单独列出,这也是出于安全性的考虑。
从图中可以看到,静态分析中找到了三个未使用的依赖,分别是@babel/plugin-syntax-import-assertions、@babel/preset-react和@babel/preset-typescript,这是对的,因为在实际的代码中,它们其实是被这样引入的:
export async function transpileToStandardJS(sourceCode: any, rootPath: string, filename = 'source.js') {
const path = require('path');
const __dirname = path.
这是Deplens引入Babel将ES版本较高的代码及TypeScript代码转换成标准JS语法的核心函数,它的目的是为了方便后续Terser的优化解析和AST树的生成,而在这个函数中,就出现了上面所说的三个“未使用”的插件。
可以看到,这三个插件实际上是通过resolvePlugin这个由Babel底层提供的自定义插件导入接口被引入的,Babel底层最后到底怎么引入它们的我们管不到,但在这里,显然是出现了一个标准的“字面量引入”场景了,于是Deplens在静态分析的过程中,就会记录这几行可疑代码的位置,并在后续的复核环节中,让AI重点检查这部分代码片段,AI会读取目标代码所在行数的前后共计20行上下文,并结合证据链等信息,综合推导出一个结论。
这个结论被系统底层捕捉,并加入置信度的计算,最终得出了一个较高的置信度结果,所以在最后的结果展示中,它们会被归类为Reclassified as confirmed used的类型,即它们被重新分类为已确认使用,而不是静态分析出的“未使用”。
至此,AI复核这个功能就真正的达到了我最开始对它的预期,通过利用AI的推理和分析能力,大大提高了分析结果的准确性,扩大了可分析的范围。
首先是RAG的问题,先给一个结论——我并没有使用真正意义上的RAG。
真正意义上的RAG,应该是先将相关内容转成向量化的形式存储,并在AI需要时进行相关度检索,最终喂给AI。但我在实现二次复核这个过程时,并没有完整的实现这一整套流程。
最主要的原因就是没必要,因为Deplens本身已经能够实现RAG前期的相关性检索这一过程了,无需依靠向量化、相关性检索等一系列流程,AI通过读取证据链,来获取一个依赖的全部相关信息,并且能够利用代码片段读取工具,精准的读取相关代码上下文,这些原本需要靠复杂的处理才能达到的结果,在代码这种逻辑性强、信息密集性高的文本类型中,我们不需要用传统手段实现RAG功能,只需要充分利用Deplens本身的功能特性,加上一些工具封装,就能够达到类似RAG的效果。
因此,我没有实现真正意义上的RAG流程,不过最终的目的都是一样的,AI能够检索和获取有用的信息作为自己的知识库,纵向扩大思考面,这也是一个好事。
其次,再来聊一下我是否真的实现了"Agent产品",答案很显然——我做的不是单纯的Agent产品,而是利用Agent加强了传统产品的功能。
作为一个Agent,它显然是合格的,在review模式和AI复核环节中,它具有记忆,能够调用工具,能检索相关信息,能进行思考和决策,并且具有一套完整的workflow流程。
但它也不仅仅是一个Agent产品,而是利用自身的特点,为传统的功能赋能,扩展了功能点,并反哺了原有的功能点,这是这个Agent在这个产品中的核心价值——扩展和赋能,而不是单纯的套壳或者“为了做而做”。
其它问题现在暂时想不到,等我以后什么时候想到了再补充吧,先在这里画个饼(
对于这一整个Agent实践,我最后是比较满意的,一个是因为它确实达到了我的预期,另一个是它也让我对Agent的整个工作原理和流程有了更加深刻的印象,在现在这个AI和Agent日益变强的今天,了解一下Agent产品,更加有利于我提升自己的技术实力,也能优化很多的流程。
最后,希望AI Agent领域越来越好吧。