1. 项目缘起:从“文档债”到“自动化”的救赎
做后端开发的朋友,尤其是用SpringBoot这类框架的,对“接口文档”这四个字恐怕是又爱又恨。爱的是,一份清晰、准确的文档是前后端高效联调的基石;恨的是,维护文档这事儿,太磨人了。需求一变,代码一改,文档就得跟着更新。稍不留神,文档就滞后了,成了“文档债”。我自己在维护一个基于若依(RuoYi)框架的管理系统时,就深受其苦。若依本身是个非常优秀的开源后台管理系统解决方案,权限、菜单、代码生成一应俱全,但接口文档这块,还是得靠手动。
传统的做法,要么是在代码里写Swagger(现在叫SpringDoc)注解,要么是单独维护一个Word或Markdown文件。Swagger注解确实能生成在线API文档,但它有几个痛点:第一,注解侵入性强,代码里到处都是@ApiOperation、@ApiParam,看着有点乱;第二,生成的在线文档样式固定,想导出为离线文档(比如给测试或产品同学)还得额外操作;第三,对于已经成型的、没有加Swagger注解的老项目, retrofit的成本不低。而手动维护的离线文档,同步问题就更突出了。
于是我就想,有没有一种更“懒”但更有效的方法?能不能让AI来干这个活儿?我的核心诉求很明确:输入是若依项目里原生的、干净的Controller层Java代码,输出是一份结构清晰、可直接使用的Markdown格式接口文档。整个过程要自动化,最好能集成到CI/CD流程里,代码一提交,文档就自动更新。这就是“Controller进,Markdown出”的由来。它不依赖特定的注解,而是通过分析代码结构、方法签名、参数名、返回值类型等元素,结合大语言模型(LLM)的理解能力,“读懂”代码并生成描述。
2. 核心设计:如何让AI“读懂”Controller
这个项目的核心难点不在于调用某个AI接口,而在于设计一套可靠的“翻译”流程,把Java代码的结构化信息,转化成AI能有效理解并生成高质量文档的提示(Prompt),最后再整理成标准的Markdown。整个过程可以分为四个核心环节:代码解析、信息结构化、AI理解与生成、后处理与格式化。
2.1 代码解析:从.java文件到抽象语法树
第一步,我们必须把Java源代码从文本变成机器可理解的结构化数据。这里不能简单地用正则表达式去匹配,因为Java语法复杂,正则很难覆盖所有边界情况,比如嵌套的泛型、复杂的注解等。最可靠的工具是抽象语法树。
我选择了JavaParser这个库。它轻量、易用,能完美地将一个.java文件解析成一棵AST。通过这棵树,我们可以精准地定位到:
- 类定义:获取Controller类的名称、
@RequestMapping或@RestController注解中的基础路径(/system/user)。 - 方法定义:遍历类中的所有public方法。识别哪些是接口方法(通常带有
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping,@RequestMapping注解)。 - 方法细节:提取方法的名称、返回类型、参数列表。对于每个参数,获取其类型、参数名以及它可能携带的注解,如
@RequestBody,@RequestParam,@PathVariable。特别要注意@RequestParam的value或name属性,这决定了URL参数名。 - 注解信息:虽然我们不强制要求Swagger注解,但如果代码里已经写了
@ApiOperation或JavaDoc注释(/** ... */),这些是极佳的补充信息,优先级最高,应优先提取。
// 示例:使用JavaParser解析一个Controller方法 CompilationUnit cu = StaticJavaParser.parse(new File("UserController.java")); List<MethodDeclaration> methods = cu.findAll(MethodDeclaration.class); for (MethodDeclaration method : methods) { // 检查方法上是否有Spring Web注解 Optional<AnnotationExpr> getMappingAnnotation = method.getAnnotationByName("GetMapping"); if (getMappingAnnotation.isPresent()) { String methodName = method.getNameAsString(); String returnType = method.getType().asString(); // ... 进一步提取参数等信息 } }这个过程相当于给AI准备了一份关于接口的“原始数据清单”。
2.2 信息结构化:构建AI的“输入菜单”
从AST提取出来的信息是零散的、面向语法层面的。我们需要把它们重新组织成一份对AI友好的“需求说明书”。我设计了一个简单的JSON结构来承载这些信息:
{ "controllerClass": "UserController", "basePath": "/system/user", "interfaces": [ { "name": "getUserById", "httpMethod": "GET", "path": "/{userId}", "returnType": "ResponseEntity<UserVO>", "parameters": [ { "name": "userId", "type": "Long", "annotation": "PathVariable", "required": true, "description": "" // 初始为空,等待AI或JavaDoc填充 }, { "name": "format", "type": "String", "annotation": "RequestParam", "required": false, "defaultValue": "json" } ], "javaDoc": "根据用户ID获取用户详细信息", "rawCodeSnippet": "public ResponseEntity<UserVO> getUserById(@PathVariable Long userId, @RequestParam(required=false, defaultValue=\"json\") String format) { ... }" } ] }这个结构化的JSON有几个关键点:
- 分离关注点:明确区分了类级别信息(basePath)和方法级别信息。
- 保留原始代码片段:
rawCodeSnippet字段非常重要。AI在理解复杂或自定义的参数类型(如UserQueryDTO)时,光看类型名可能不够,提供方法签名或相关类的定义片段能极大提升理解准确性。 - 为AI留白:
description字段初始为空。我们的目标是让AI根据方法名、参数名、类型和原始代码,推断出这个参数是干什么的。
提示:在构建这个结构时,对于复杂的自定义对象(如
UserVO,UserQueryDTO),最好能同时解析这些类的定义,并将其字段信息也作为上下文提供给AI。这能避免AI对UserQueryDTO生成“用户查询数据传输对象”这种空洞的描述,而是能具体到“包含用户名、手机号、状态等查询条件的对象”。
2.3 AI理解与生成:设计精准的Prompt
这是项目的灵魂所在。我们不能简单地把JSON扔给AI说“写个文档”,那样生成的内容会非常随意,格式也不统一。Prompt工程在这里至关重要。
我的Prompt模板大致如下,它结合了系统指令、结构化数据和输出格式要求:
你是一个专业的Java后端开发工程师,擅长编写清晰、准确的API接口文档。 请根据以下提供的Java Controller接口信息,为每个接口生成详细的Markdown格式文档。 ## 接口上下文 - 项目框架:SpringBoot + 若依(RuoYi)管理系统 - 基础路径:`{{basePath}}` - 当前Controller:`{{controllerClass}}` ## 接口列表详情 {{#each interfaces}} ### 接口 {{@index_1}}: {{this.name}} - **HTTP方法**: {{this.httpMethod}} - **路径**: `{{this.path}}` (最终URL为: `{{../basePath}}{{this.path}}`) - **返回类型**: `{{this.returnType}}` - **方法原始代码**: ```java {{this.rawCodeSnippet}}- 参数列表: {{#each this.parameters}}
{{this.name}}({{this.type}}, {{this.annotation}}): [请根据参数名、类型、注解及代码上下文,用一句话描述该参数的作用和规则。例如,userId是路径变量,表示用户唯一标识。] {{/each}} {{/each}}
输出要求
- 为每个接口生成一个独立的Markdown二级标题,格式为
## {{httpMethod}} {{basePath}}{{path}}。 - 在每个接口标题下,按顺序包含以下小节:
- 功能描述:用一两句话概括这个接口是做什么的。请参考方法名和JavaDoc(如果提供)。
- 请求参数:以表格形式列出所有参数。表格列包括:参数名、位置(Path/Query/Body)、类型、是否必填、默认值、说明。说明栏必须填写,要结合业务逻辑进行推断。
- 请求示例:给出一个完整的、可读的请求示例(如cURL命令)。
- 响应示例:给出一个典型的成功响应JSON body示例。对于返回
ResponseEntity<T>或R<T>的,请展示T的数据结构。 - 可能的错误码:推断并列出几个常见的业务或系统错误码(如400-参数错误,404-资源不存在,500-系统异常)。
- 所有描述性语言需专业、简洁、无歧义。
- 整个输出请使用纯Markdown格式。
这个Prompt的关键在于: - **角色设定**:让AI进入“专业开发者”的角色。 - **提供充足上下文**:框架、基础路径、原始代码,这些信息能约束AI的想象,使其生成的内容更贴合技术栈。 - **结构化指令**:明确要求按接口拆分,并规定了每个接口文档必须包含的子章节和格式(特别是表格),保证了输出的一致性。 - **引导推理**:在参数部分,不是直接给描述,而是给出一个推理任务的描述(“[请根据...用一句话描述]”),这能激发AI的分析能力,生成比“用户ID”更有价值的说明,比如“用户的唯一标识符,必须为正整数”。 ### 2.4 后处理与格式化:从AI文本到标准文档 AI返回的是一大段Markdown文本。我们还需要做一些后处理工作,使其成为一份真正可用的文档: 1. **格式校验与修正**:检查生成的Markdown是否符合规范,表格是否对齐,代码块语言标识是否正确。有时AI可能会漏掉某个小节,可以用简单的规则进行补全或提示。 2. **信息融合**:如果原始代码中已经包含了JavaDoc或Swagger注解,后处理阶段应该用这些高可信度的信息,去覆盖或补充AI生成的内容。例如,JavaDoc中的`@param userId 用户主键ID`就应该直接作为参数的最终描述。 3. **文档聚合**:一个Controller会生成一个MD文件。我们可以设计一个索引生成器,遍历项目所有Controller,生成一个总的`README.md`,包含所有接口文档的链接,形成完整的API文档站点结构。 4. **集成与自动化**:将整个流程脚本化(Python或Java程序),并集成到Maven/Gradle构建生命周期或Git的pre-commit/CI流水线中。实现“代码提交,文档同步更新”。 ## 3. 技术选型与实战踩坑 有了设计思路,接下来就是选型和实现。这里有几个关键决策点和踩过的坑。 ### 3.1 AI模型选择:成本、效果与可控性的平衡 最初我尝试了云端大模型API,如OpenAI的GPT-4或 Anthropic 的 Claude。它们的理解能力和生成质量确实很高,对于复杂业务逻辑的推断很到位。但问题也很明显: - **成本**:每次生成文档都需要调用,对于接口数量多的项目,是一笔持续的开销。 - **网络与延迟**:依赖外部API,在内网开发环境或CI流水线中可能受限。 - **数据安全**:虽然只是代码片段,但将公司项目代码发送到第三方云服务,有些场景下存在合规风险。 因此,我转向了**本地化部署的大模型**。目前有几个不错的选择: - **Ollama**:极其方便的本地大模型运行框架,一条命令就能拉取和运行模型。推荐使用 `qwen:7b`、`llama2:7b` 或 `codeqwen` 这类在代码理解上表现较好的模型。 - **LM Studio**:图形化界面,对不熟悉命令行的开发者更友好,同样支持多种GGUF格式的模型。 - **DeepSeek-Coder** 等开源代码专用模型:这类模型对代码的语法、语义理解更深,生成文档时更准确。 > **实操心得**:对于接口文档生成这种任务,对模型的“创造力”要求并不高,反而对“准确性”和“格式遵从性”要求更高。经过测试,6B-7B参数量级的模型,在给出清晰Prompt的情况下,完全能够胜任。本地部署的 `Qwen-7B-Chat` 或 `CodeQwen-7B` 模型是不错的选择,它们在代码理解和指令跟随上表现良好,且运行在消费级显卡(甚至只靠CPU)上也能接受。 ### 3.2 解析层的边界情况处理 在代码解析阶段,会遇到各种“不标准”的写法,AI生成流程的健壮性很大程度上取决于这里。 **坑1:泛型与复杂返回类型** 若依框架常用 `R<T>` 或 `ResponseEntity<T>` 来包装返回结果。解析时不能只看到 `R`,必须提取出泛型参数 `T`。 ```java public R<PageInfo<UserVO>> list(UserQueryDTO query) { ... }解析器需要能识别出PageInfo<UserVO>这个嵌套泛型,并将其作为有效的返回类型信息传递给AI。否则AI可能只会描述“返回一个R对象”,而不知道里面具体的数据结构。
坑2:参数绑定注解的多样性Spring提供了多种参数绑定方式,解析器需要全面识别:
@RequestParam(required = false, defaultValue = "0") int pageNum@PathVariable("id") Long userId(注意注解内的别名)@RequestBody @Valid UserDTO user(可能带有校验注解)HttpServletRequest request,Model model(这类内置对象通常不需要写入文档)- 无注解的参数:在Spring MVC中,这可能被绑定到简单类型的请求参数上,需要按
@RequestParam处理。
坑3:方法继承与接口实现有些Controller可能实现了某个基类或接口中的通用方法。单纯分析一个类文件可能漏掉这些方法。一个更健壮的方案是结合编译后的Class文件进行分析,或者使用Spring自身的RequestMappingHandlerMapping在应用运行时获取所有端点信息。但对于静态分析工具来说,前者更可行。
3.3 Prompt工程的迭代优化
最初的Prompt很简单,结果AI经常“放飞自我”,生成一些无关内容或者格式混乱。经过多次迭代,才稳定到上文提到的版本。几个优化点:
- 明确拒绝指令:在系统指令中加入“你只需要生成文档内容,不要生成任何额外的解释、介绍或总结段落”,有效避免了AI在文档前后加废话。
- 示例的力量(Few-Shot):在Prompt中给一个完美的接口文档示例,能显著提升AI输出的格式一致性。这就是“少样本学习”。
- 分步骤指令:将“分析参数”和“生成表格”分开要求,比笼统地说“生成文档”效果更好。AI的思维链更清晰。
- 温度(Temperature)设置:对于文档生成这种需要确定性和一致性的任务,将温度参数调低(如0.1-0.3),可以减少随机性,让输出更稳定、可预测。
4. 集成与落地:让流程飞起来
工具做出来,最终目的是要用起来,而且要无缝集成到开发流程中,不能增加额外负担。
4.1 本地开发:一键生成
我首先将其做成了一个Maven插件。开发者在本地执行mvn ruoyi-doc:generate,插件会自动:
- 扫描
src/main/java下所有标注了@RestController的类。 - 解析并生成结构化JSON。
- 调用本地部署的Ollama服务(通过HTTP API)。
- 将生成的Markdown文档输出到
target/api-docs目录,并按模块分文件夹存放。
这样,开发者在完成一个Controller的编写或修改后,可以随时运行命令,立刻看到最新的文档效果,进行微调。
4.2 持续集成:自动化保障
真正的威力体现在CI/CD流水线中。我们在GitLab CI(或Jenkins)中配置了一个Job,监听master或develop分支的合并请求(Merge Request)。
- 触发条件:当MR的目标分支是
master/develop,且修改的文件包含*Controller.java时,自动触发文档生成Job。 - 执行流程:CI Runner拉取代码,运行Maven插件生成最新的Markdown文档。
- 文档比对与提交:将新生成的文档与仓库中文档目录(如
docs/api/)下的现有文档进行比对。如果有变化,则自动创建一个新的提交,更新文档文件,并推送到仓库。这个提交可以标记为[CI] Update API Docs。 - 通知:将文档更新后的预览链接(如果部署了静态站点)或变更内容摘要,评论到MR中,提醒代码审查者。
这套流程彻底将开发者从手动维护文档的负担中解放出来,确保了文档与代码的实时同步,实现了“文档即代码”。
4.3 效果展示与对比
最终生成的Markdown文档格式清晰,直接可以放入Wiki或部署为静态站点(用MkDocs、Docsify等)。以下是一个片段示例:
## GET /system/user/{userId} ### 功能描述 根据用户唯一标识符(ID)查询用户的详细信息。 ### 请求参数 | 参数名 | 位置 | 类型 | 必填 | 默认值 | 说明 | | :--- | :--- | :--- | :--- | :--- | :--- | | userId | Path | Long | 是 | 无 | 用户的主键ID,必须为正整数。 | | format | Query | String | 否 | json | 响应格式,可选值为 `json`(默认)或 `xml`。 | ### 请求示例 ```bash curl -X GET 'http://localhost:8080/system/user/123?format=json' \ -H 'Authorization: Bearer your_token_here'响应示例(成功)
{ "code": 200, "msg": "操作成功", "data": { "userId": 123, "userName": "zhangsan", "nickName": "张三", "email": "zhangsan@example.com", "phonenumber": "13800138000", "status": "0", "createTime": "2023-10-01 12:00:00" } }可能的错误码
400 Bad Request: 请求参数无效,如userId格式错误。404 Not Found: 指定ID的用户不存在。500 Internal Server Error: 服务器内部错误。
对比手写或Swagger UI导出的文档,AI生成的描述在“说明”一栏往往更贴近业务语义(因为它尝试去“理解”参数名`userId`的含义),而不是简单的技术描述。表格和示例的格式也非常规范统一。 ## 5. 局限性与未来展望 当然,这个方案并非银弹,目前仍有其局限性: 1. **理解深度依赖代码清晰度**:如果方法名和参数名起得随意(如`queryData(Map<String, Object> params)`),AI也难以生成准确的描述。良好的编码规范是基础。 2. **复杂业务逻辑的盲区**:AI只能基于代码“签名”和有限的上下文推断功能。对于接口内部的复杂业务规则、状态变迁、权限校验细节等,无法自动生成。这部分仍需人工补充说明。可以考虑扩展Prompt,让AI在文档中提示“此接口涉及权限校验,需要`sys:user:view`权限”,但这需要从Spring Security注解中提取信息。 3. **模型的一致性**:不同模型,甚至同一模型的不同版本,生成的结果可能有细微差异。需要通过严格的Prompt和后期模板来约束。 4. **非RESTful接口**:对于WebSocket、GraphQL等非HTTP接口,当前方案不适用。 未来的优化方向可以包括: - **多轮交互与人工修正**:生成初稿后,提供一个简单的界面让开发者可以快速审核、编辑AI生成的内容,并将修正反馈给模型进行微调(在线学习),让模型越来越懂你的项目。 - **结合测试用例**:如果能分析相关的单元测试或集成测试,AI可以从中提取出更具体的请求/响应示例,甚至包括边界情况和错误场景。 - **生成多种格式**:除了Markdown,是否可以一键生成Postman Collection、OpenAPI 3.0 (Swagger) spec文件,满足不同工具链的需求。 这个项目的核心价值,不在于替代开发者思考,而在于**将开发者从重复、机械的文档编写劳动中解放出来**,让他们能更专注于代码逻辑和业务创新。它证明了,在软件开发中,那些看似需要“人类智能”的繁琐任务,正逐渐可以被AI以一种可靠、高效的方式接管。“Controller进,Markdown出”,只是一个开始。