本文深入探讨了在开发过程中,大模型(如AI编程助手)在理解项目二方包代码时面临的挑战,并通过一个实际案例复盘了从失败到成功驯服AI的全过程。文章尝试使用Cursor等AI工具直接生成调用二方包接口的代码,但因AI无法读取未在当前工程中打开的依赖源码,导致生成的代码错误频出,甚至出现幻觉式编码。为解决此问题,文章探索了多种方案,最终发现本地反编译MCP方案最为有效,能够精准解析二方包中的类与方法,显著提升代码生成的准确性和可用性。文章强调,应将AI视为需辅助的“工具人”,通过提供良好上下文与工具来增强其能力,而非期待其无所不能。
背景
近期有一个需求中需要查询用户订单状态,这个功能本身很简单,是一个经典的依赖下游场景,需要读取交易订单二方包的代码逻辑,并分析出入参出参的含义,并判定用户指定的订单是否已经进入终态。
在大多数日常工作开发过程中,会遇到很多需要调用各种下游提供的接口,对二方包有依赖的场景(例如优惠券、会员、订单……)。这种情况下,大模型是否能够理解开发者的诉求呢?
起初,面对如此简单的需求,AI编程助手被寄予厚望,结果却上演了一出大型“翻车现场”。本文复盘了从失败到驯服AI的全过程,揭示如何让大模型理解二方包。文中对部分方法名称做了脱敏处理,可能存在前后不一致,但这不影响核心论述点。
直接使用Cursor — 失败
文章尝试将交易接口的内部文档链接提供给Cursor,并使用/doc功能添加了交易相关的两个接口页:查询订单(在线库)和查询订单(历史库)(这两个网页是内部办公网页面,外部用户可能无法访问)。文章清晰地描述了需求:“请帮助实现一个批量查询订单的方法,先查询在线库,若查询不到则再去历史库获取……”以下是几轮对话记录:
user:
请帮我实现一个批量查询订单的方法,查询指定userId和List<long>subOrderIdList 下的订单结果。请在TpClientImpl中实现。请注意方法的逻辑。需要先在通过订单在线库中查询,使用xxxService.queryMainAndDetail,如果查询出来的结果中没有指定orderIdList的信息,请收集之后统一再次使用订单历史库查询,使用xxxBaseService.queryMainAndDetail。根据网页中给出的返回值的逻辑。最终将两者查询出来的结果统一按照传入的 subOrderIdList 的顺序返回一个订单列表。请先不要写代码,先规划任务并写入到.ai/指定订单查询task.md。
Cursor:
给出了需要 实现一个批量查询订单的方法queryBizOrderList,下面是任务规划文档,写的没啥问题:
# 指定订单查询任务规划``## 需求概述``实现批量查询指定 userId 和 List<long> subOrderIdList 下订单结果的方法 queryBizOrderList。``## 业务逻辑``1. **先查询在线库**:使用 xxxService.queryMainAndDetail 方法查询传入的所有订单ID``2. **收集未找到的订单ID**:对比查询结果与传入的订单ID列表,收集未查询到的订单ID``3. **查询历史库补充**:使用 xxxBaseService.queryMainAndDetail 方法查询未找到的订单ID``4. **合并结果**:将在线库和历史库的查询结果合并``5. **按序返回**:按照传入的 List<long> subOrderIdList 的顺序重新排序并返回订单列表``## 技术实现任务分解``### 任务1:了解现有服务和方法签名``- [ ] 确认 xxxService.queryMainAndDetail 方法是否存在,如不存在需要定义``- [ ] 确认 xxxBaseService(历史库服务)是否存在,如不存在需要引入``- [ ] 确认 xxxBaseService.queryMainAndDetail 方法签名``- [ ] 了解查询结果的数据结构和返回值类型``### 任务2:设计方法签名``java``/**``* 批量查询指定用户的订单列表``* @param userId 用户ID``* @param subOrderIdList 子订单ID列表``* @return 按照传入顺序返回的订单结果列表``* @throws TCException 查询异常``*/``List<SingleQueryResultDO> queryBizOrderList(long userId, List<Long> subOrderIdList) throws TCException;`### 任务3:实现核心逻辑``- [ ] 参数校验:检查 userId 和 subOrderIdList 的有效性``- [ ] 在线库批量查询:调用 xxxService.queryMainAndDetail- [ ] 结果分析:识别哪些订单ID没有查询到结果``- [ ] 历史库补充查询:对未找到的订单ID调用 `xxxBaseService.queryMainAndDetail– [ ] 结果合并:将两次查询的结果合并到一个Map中- [ ] 顺序重排:按照传入的 subOrderIdList 顺序构建返回列表### 任务4:异常处理和日志- [ ] 添加适当的异常处理逻辑– [ ] 对查询不到的订单ID记录ERROR日志(根据记忆中的用户偏好)- [ ] 使用 guard clauses(提前返回)优化代码结构– [ ] 添加 TraceLogUtil 链路追踪- [ ] 统计在线库和历史库的查询耗时### 任务5:性能优化考虑- [ ] 如果传入的订单ID列表为空,提前返回– [ ] 避免不必要的历史库查询(如果在线库已查询到所有订单)- [ ] 考虑是否需要并行查询在线库和历史库### 任务6:更新接口定义- [ ] 在 xxxClient 接口中添加 `queryBizOrderList` 方法定义– [ ] 确保方法签名的一致性### 任务7:单元测试– [ ] 编写测试用例- [ ] 测试场景:全部在在线库找到– [ ] 测试场景:部分在在线库,部分在历史库- [ ] 测试场景:全部需要从历史库查询– [ ] 测试场景:部分订单查询不到- [ ] 测试场景:异常处理## 依赖和风险点### 需要确认的依赖1. xxxBaseService 服务:需要确认历史库查询服务是否已配置2. **queryMainAndDetail 方法**:需要确认具体的方法签名和返回值结构3. 数据一致性:确认在线库和历史库返回的数据结构是否一致### 潜在风险1. 性能风险:批量查询可能影响性能,需要考虑分页或限制查询数量2. 超时风险:历史库查询可能较慢,需要设置合适的超时时间3. 数据不一致:在线库和历史库的数据可能存在不一致## 实现优先级1. 高优先级:核心查询逻辑实现2. 中优先级:异常处理和日志记录3. 低优先级:性能优化和监控`
并且要求确认依赖关系:
-
xxxBaseService服务需要引入; -
queryMainAndDetail方法签名需要确认; -
批量查询的性能和超时控制。
这里做得不错。
user:
请先阅读二方包代码xxxService.queryMainAndDetail以及xxxBaseService.queryMainAndDetail调用的出入参是什么,再写代码逻辑。
Cursor:
其阅读代码后给出了判定结果:
-
xxxService.queryMainAndDetail方法不存在; -
xxxBaseService服务不存在;
代码库中没有找到历史库查询服务xxxBaseService;
-
queryMainAndDetail方法在整个代码库中都不存在。
走到这里,判定结果说明Cursor只读取了工程代码库的代码,并不能读取明确指定的二方包中的方法。另外,由于之前曾有查询单个指定ID订单的方法,Cursor提出基于现有方法实现批量查询:使用循环调用getBizOrderById方法实现批量查询。
这表明Cursor无法读取二方包中的代码,于是下一轮对话中明确使用了全限定名。
user:
这两个类以及方法是二方包中的方法,二方包已在pom中以及Configuration引入,需要你阅读并使用com.tb.xxx.yyy.api.xxxService和com.tb.ccc.ddd.service.xxxBaseService,调用前面我提到的queryMainAndDetail方法。
Cursor:
开始随意实现,导入的包路径是LLM根据代码库中之前client防腐层中写过的getBizOrderById方法中引入的xxxService幻想出来的。
例如,使用了com.tb.xxx.yyy.api.xxxBaseService,而没有使用正确的com.tb.ccc.ddd.service.xxxBaseService。
Cursor发现报错后,再次进行猜测,转成了com.tb.xxx.yyy.api.history.xxxBaseService,纯粹根据“历史库查询”这一描述进行幻觉式编码。
在此阶段,文章介入手动修正了import路径,但并未直接告知大模型xxxService.queryMainAndDetail以及xxxBaseService.queryMainAndDetail的完整方法签名,仍抱有对AI能力的幻想。
user:
好了,我已经修正了引入了xxxService和txxxBaseService的导入路径,请你解析我指定的方法,根据出入参写出正确的调用代码。
Cursor:
Cursor并未直接读取xxxService.queryMainAndDetail以及xxxBaseService.queryMainAndDetail的方法签名,而是根据编译后返回的错误信息才得知其传入的参数不正确。以下是其思考过程:
好的,现在我看到了方法签名的错误。需要使用 QueryBizOrderDO 对象而不是直接传入 List<Long>。让我查看一下 QueryBizOrderDO 的结构:``我需要了解 QueryBizOrderDO 的结构。让我搜索更多相关信息:``让我继续搜索 QueryBizOrderDO 的相关信息:``看起来我无法在代码库中找到 QueryBizOrderDO 的具体实现。
Cursor发现其根本不知道应该如何构造这个入参。进行到这里已经非常确认Cursor无法读取到jar包中的代码(即便已明确使用了全限定名)。
进行到这里,文章发现与AI结对编程更像是辅导一个记性不佳且充满幻想的孩子完成作业。此外,观察到入参xxxQueryXxxOrderDO定义有两千多行,返回对象xxxOrderInfoResultDO内部也包含多层逻辑。
其中要用到的订单信息bizOrderDO又有上万行。若将这些代码全部提供给AI,预计上下文Token将严重超限。为了优化Token使用效率,此方案被放弃。
理论上可以考虑直接将API网页导出为Markdown形式进行尝试。然而,该网页中嵌套了语雀文档,语雀文档进一步阐述了出参含义,且网页信息与实际二方包不符(例如订单ID,网页显示为long,实际二方包需传入long[]),因此该方案被放弃。
【世上无难事,只要肯放弃(不是)】
对于这个需求,看似无往而不利的AI表现得像“智障”一样,这揭示了一个问题:对于未在当前工作区打开的、作为编译依赖(二方包)存在的代码,LLM几乎是“盲人”。它无法像IDE那样“Go to Definition”,也无法理解那些复杂对象的内部结构。指望通过提供文档链接来解决,在面对复杂的内部系统时,基本等于缘木求鱼。
经过反思,既然AI无法“看见”二方包代码,便需要设法为其“开天眼”。文章调研并采用了以下几种解决方案。
让大模型理解二方包:解决方案探索
此前的LLM幻觉问题,根源在于AI工具无法直接理解二方包的说明。这些AI工具常会尝试拼凑内容,假装完成了任务,若不仔细核查,甚至可能误认为其表现尚可。这类似于业务通过数据营造欣欣向荣的假象,或员工向管理者汇报任务完成,无论采用何种方式,最终目的都是呈现结果。🐶
前面提到,对于依赖下游二方包的需求,LLMs需要获取外部依赖知识才能生成代码,因此需要提供工具使其了解这部分知识。类比一位刚接手代码仓库的新同学,其熟悉项目的方式通常是查阅项目知识库,并通过IDE进行源码跳转。
对于大模型而言,同样存在两种解决方案,对比如下:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 构建项目知识库:维护当前项目所需内容及外部依赖说明,可将接口类、出入参类都拷贝进去。 | 1. 能够让LLM对当前项目有一个全面的理解。 2. 人工可加入理解说明,因接口参数说明不全面时需人工解读。 |
1. 建立的知识库对当前项目理解深入,但对外部依赖理解仍局限于自身使用。 2. 需全集团维护公共完整知识库才能很好解决外部依赖问题。 3. 知识召回准确度可能存在问题,可能召回不需要的内容。 |
| 实现类似IDE的索引查询:开发时能像人一样点击到定义处了解具体类信息。 | 1. 召回的类数据一定是最准确的。 2. 可召回类依赖数据,确保LLM对某个类的理解全面完整。 |
1. 知识强依赖于类的说明和注解,错误的注解会影响最终效果。 2. 包需下载到本地.m2,分析可能Token耗用量较大。 |
为使AI工具能够获取二方包内容并实现需求,文章调研了以下几种方案。
▐方案一 & 二:大力出奇迹 — 手动“投喂”源码
最直接的思路是缺啥补啥。既然LLM看不到二方包的源码,那么直接将源码复制给它即可。这显然是最直接的方案,此处不再赘述手动将源码复制到聊天框的方案。
文章尝试了公司内部的AoneCopilot和通用的Claude,发现它们提供了一些无需手动粘贴即可读取二方包代码的方案。
-
方案一:使用AoneCopilot
1. 找到选定文件后右键选择直接发送文件到chat
AoneCopilot能够分析指定文件的代码信息,并能解析二方包中的方法。尽管其识别出入参为QueryBizOrderDO,但由于未手动将二方包中的QueryBizOrderDO类添加到聊天上下文,Agent并未在二方包中查找此DTO,而是在项目代码中读取,最终未能找到入参的定义。
此方案与手动粘贴代码给Agent无异,只能读取手动添加进去的文件,对于未手动添加到chat的文件Agent是不能读取的。

可能会有疑问,如果这个二方包没有上传源码的情况,Agent能否拿到代码内容呢?经过测试,发现可以获取到代码内容。根据AoneCopilot文档说明:当使用“Add File To Chat”功能时,IDE已完成文件读取和反编译工作,并将结果直接提供给AoneCopilot,因此此时可直接获取反编译后的代码内容。
2、告知使用read_file工具
可以指定全限定名,并告知其使用read_file工具读取代码。此方案可以让AoneCopilot分析此类的代码信息,并且也能够分析未指定的入参的类的信息。此方案看起来已比较智能,但貌似只能读取有源码的二方包中的内容。

-
方案二:使用ClaudeCode
-
要求ClaudeCode阅读源码,模型内部应做了特殊处理(估计也提供了类似 AoneCopilot 的read_file工具),其能够直接定位到.m2文件中的下载的jar包,并且CC能自动读取未指定的入参的类,表现相当出色。但同样,貌似只能读取有源码的二方包中的内容。


总结:
评价: 简单粗暴,大力出奇迹。对于一次性的、依赖不多的临时需求,此方法立竿见影。只要将读取源码(或者使用read_file)这个prompt添加到rules,模型基本就能够实现。
缺点: 不是很优雅,每次提问都需要重复“投喂”,如果依赖关系复杂,涉及十几个类,那么复制粘贴将是体力活,且容易超出模型的上下文窗口限制。另外也要求有二方包的源码。
▐方案三:自己动手,丰衣足食 — 本地反编译MCP
手动投喂过于笨拙,是否存在更优雅的方式?答案是肯定的。通过MCP工具,LLM能够像IDE一样“查看定义”。就像程序员在IDE中点击一下就能看到各种定义。
核心思路分两步:
-
建索引 (Index): 在项目初始化时,通过
mvn dependency:tree扫描项目所有依赖的二方包,再通过jar tf命令解析每个jar包,建立一个“类全名 -> JAR包路径”的本地映射索引。 -
反编译 (Decompile): 提供一个
Java Class Analyzer工具。当LLM需要分析某个类(比如QueryBizOrderDO)时,它会调用这个工具。工具根据索引找到对应的JAR包,然后调用cfr之类的反编译库,将.class文件实时反编译成Java源码,再返回给LLM。
具体MCP由长济提供,由于此工具目前只在内部提供,此处仅介绍基本原理和使用效果,不详细说明安装方法。
- 实现:

-
初始化项目建立二方包的索引数据,通过 mvn dependency:tree -DoutputType=text -DappendOutput=false -o 获取当前项目依赖的所有二方包,通过 jar tf “${jarPath}” | grep ‘.class$’获取所有类名,建立class->二方包的索引。
-
提供一个类分析的工具,通过cfr进行类的反编译即可拿到对应的类。
Prompt说明需要加上使用工具(提醒有这么个工具可以分析类和函数的说明):当要分析某一个类的时候可以使用 Java Class Analyzer进行分析,整个项目最开始需要告知调用 java class indexer 初始化。【这个语句直接添加到项目维度下的rules即可】。

可以看到,通过索引工具和类分析工具,大模型非常顺利地获取了完整的类、出入参的说明。要求给出查询代码,也给出了非常好的调用示例,重要的是import都没有问题,代码是直接可用的。
使用结果:

总结:
-
评价: 这才是真正的“授人以渔”!通过这种方式能够很好地解决LLM不理解二方包内信息的问题,避免LLM幻觉胡乱发挥的问题;一方面减少了技术方案中的各种资料库和外部说明(文档想要做到实时保鲜太难了),更重要的是提高了代码生成的准确率和可用性。生成的代码质量和准确率飙升,连
import都书写得明明白白。 -
优点: 一次配置,长久受益。最重要的是,它不依赖任何外部系统,只要是本地的依赖包,无论是集团内部的还是开源的,通通都能搞定。
干活:配好反编译MCP后再给AI一次机会
使用方案三,将MCP和rules配置好之后,大模型非常顺利地完成了订单查询需求。它能够正确import,并正确构造请求入参,解析出参,不再像一开始是完全不可用的状态。
-
1. 更新了xxxClient接口
-
添加了
queryBizOrderList方法的定义; -
方法签名:
List<BizOrderDO> queryBizOrderList(long userId, List<Long> subOrderIdList) throws TCException; -
添加了必要的import和注释。
-
2. 完善了xxxClientImpl实现
查询流程:
-
先查在线订单库:使用
xxxService.queryMainAndDetail方法查询; -
收集查询结果:将查询到的订单存储到Map中,key为subOrderId,value为BizOrderDO;
-
找出缺失订单:收集在线库中没有查到的订单ID;
-
查历史订单库:对于缺失的订单ID,使用
xxxBaseService.queryMainAndDetail再次查询; -
合并结果:将历史库查询到的订单也加入到Map中;
-
按序返回:按照传入的
List<Long>的顺序,从Map中取出对应的BizOrderDO并返回。
关键特性:
-
智能查询:只对在线库没有查到的订单再查历史库,避免重复查询;
-
保持顺序:严格按照传入的subOrderIdList的顺序返回结果;
-
健壮性:包含完整的异常处理和日志记录;
-
性能优化:使用Map进行快速查找,避免重复遍历。
数据结构处理:
-
正确处理了
BatchQueryOrderInfoResultDO返回值 -
从
List<OrderInfoTO>中提取BizOrderDO -
使用
BizOrderDO.getBizOrderId()作为订单ID进行匹配
总结:从“方法清单”到“思想升华”
| 方案 | 核心思想 | 优点 | 缺点 | 适用场景 | 点评 |
|---|---|---|---|---|---|
| 手动投喂 | 缺啥补啥,人肉Copy | 简单直接,立竿见影 | 繁琐,不优雅,上下文易超限 | 临时、依赖少的简单需求 | 大力出奇迹,饲养员的快乐! |
| 本地反编译 | 赋能AI,让它和人一样能够查看定义 | 一劳永逸,精准 | 需要一次性配置开发,但Token调用可能会比较多 | 日常主力开发,复杂依赖场景 | 自己动手,丰衣足食!有Token财大气粗就没缺点 |
文章最大的感悟是:不应将LLM视为无所不能的神,而应视其为一个能力边界清晰的强大“工具人”。
手动投喂,是为其“开小灶”;打造反编译工具链,是为其构建“专属的搜索引擎”;无论哪种方式,核心都在于弥合AI基础能力与复杂多变的业务现实之间的鸿沟。
因此,当AI助手再次“犯傻”时,不应急于抱怨其“不行”。而应首先反思:是否为其提供了足够好的“上下文”和“工具”?
