第2133篇:内部AI代码助手的构建实践——为你的团队定制专属Copilot
第2133篇:内部AI代码助手的构建实践——为你的团队定制专属Copilot
适读人群:想为团队搭建专属代码助手的技术负责人和工程师 | 阅读时长:约20分钟 | 核心价值:理解代码助手的核心功能实现,基于内部代码库和最佳实践构建专属的代码AI
"GitHub Copilot不了解我们的内部框架,总给我生成通用的代码,还不如自己写。"
这是很多有自研框架的公司的痛点。Copilot、CodeWhisperer这类通用代码助手,对公开代码库训练得很好,但对你们内部的私有框架、约定和模式一无所知。
建立一个了解自家代码库的AI代码助手,是有一定规模的工程团队的重要基础设施投资。这不需要训练大模型——用RAG+精心设计的Prompt,就能让通用大模型"读懂"你们内部的代码风格和框架用法。
代码助手的核心能力设计
/**
* 内部代码助手的核心功能
*
* ===== 功能一:代码补全 =====
*
* 基于上下文(当前文件、光标位置),建议接下来的代码
* 区别于通用Copilot:能建议内部API的正确用法
* 例:输入"userService.",建议内部UserService的方法
*
* ===== 功能二:代码解释 =====
*
* 解释一段复杂的内部代码是什么意思
* 特别对新人有价值:理解旧代码库
*
* ===== 功能三:代码生成 =====
*
* 根据注释或描述生成代码
* 结合内部规范:生成的代码符合团队风格
*
* ===== 功能四:代码审查 =====
*
* 审查代码是否违反内部最佳实践
* 识别安全问题、性能问题、规范问题
*
* ===== 功能五:问答 =====
*
* 回答关于内部框架和代码库的问题
* "怎么在我们的系统里实现分布式锁?"
* 能给出符合团队规范的答案
*
* ===== 核心架构 =====
*
* 代码库索引(Embedding)→ 上下文检索 → Prompt组装 → LLM生成
* ↑ ↑
* 内部代码库 当前代码上下文
*/代码库索引构建
/**
* 代码库索引服务
*
* 把代码库里的关键信息向量化存储
* 供代码助手检索参考
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class CodebaseIndexingService {
private final EmbeddingModel embeddingModel;
private final VectorStore vectorStore;
/**
* 索引代码库
*
* 不是把每行代码都索引,而是有选择地索引高价值内容:
* 1. API接口定义(Controller, Service接口)
* 2. 核心类和方法(带注释的公共API)
* 3. 使用示例(Test类里的测试用例)
* 4. 内部文档(README, Wiki)
* 5. 设计模式和最佳实践文档
*/
public void indexCodebase(String repoPath) {
log.info("开始索引代码库: {}", repoPath);
// 1. 索引API接口
indexApiDefinitions(repoPath);
// 2. 索引核心类(带Javadoc的公共类)
indexPublicClasses(repoPath);
// 3. 索引使用示例(从测试代码提取)
indexUsageExamples(repoPath);
log.info("代码库索引完成");
}
/**
* 索引API定义
*
* 把方法签名+Javadoc作为一个索引单元
*/
public void indexApiDefinitions(String repoPath) {
// 解析所有带@RestController或@Service注解的类
// 提取方法签名和Javadoc
// 示例:索引一个API方法
String apiDoc = """
/**
* 查询用户订单列表
* @param userId 用户ID
* @param status 订单状态:PENDING/PROCESSING/COMPLETED/CANCELLED
* @param pageNo 页码,从1开始
* @return 订单分页结果
*/
public Page<OrderVO> getUserOrders(Long userId, String status, int pageNo)
""";
// 把方法信息向量化存储
String docUnit = "API方法: getUserOrders\n\n" + apiDoc;
float[] vector = embeddingModel.embed(docUnit).content().vector();
vectorStore.add(VectorStore.Document.builder()
.id("api-get-user-orders")
.content(docUnit)
.vector(vector)
.metadata(Map.of(
"type", "api_definition",
"class", "OrderService",
"method", "getUserOrders",
"repoPath", "src/main/java/com/example/service/OrderService.java"
))
.build());
}
/**
* 索引使用示例
*
* 从测试代码中提取,测试代码通常是最好的用法示例
*/
private void indexUsageExamples(String repoPath) {
// 示例:从测试类提取的使用示例
String usageExample = """
// 使用示例:查询用户订单
// 来源:OrderServiceTest.java
// 1. 查询待处理订单
Page<OrderVO> pendingOrders = orderService.getUserOrders(userId, "PENDING", 1);
// 2. 查询所有订单(status传null表示不过滤)
Page<OrderVO> allOrders = orderService.getUserOrders(userId, null, 1);
// 3. 订单VO的关键字段
OrderVO order = orders.getContent().get(0);
order.getOrderId(); // 订单ID
order.getTotalAmount(); // 订单金额(BigDecimal)
order.getStatus(); // 订单状态
order.getCreatedAt(); // 创建时间(LocalDateTime)
""";
float[] vector = embeddingModel.embed(usageExample).content().vector();
vectorStore.add(VectorStore.Document.builder()
.id("example-order-service-usage")
.content(usageExample)
.vector(vector)
.metadata(Map.of("type", "usage_example", "source", "OrderServiceTest.java"))
.build());
}
private void indexPublicClasses(String repoPath) {
// 索引关键公共类的描述和用途
// 实现略
}
}代码助手核心服务
/**
* AI代码助手服务
*
* 核心:把用户的代码问题 + 内部代码库知识 组合成高质量回答
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class InternalCodeAssistantService {
private final ChatLanguageModel llm;
private final EmbeddingModel embeddingModel;
private final VectorStore vectorStore;
/**
* 代码问答:回答关于内部代码库的问题
*/
public String answerCodeQuestion(
String question,
String currentFileContent, // 当前文件内容(提供上下文)
String currentLanguage) {
// 1. 检索相关的内部代码文档
String queryText = question;
if (currentFileContent != null && !currentFileContent.isEmpty()) {
// 结合当前文件上下文来检索
queryText = question + "\n\n当前代码上下文:\n" +
truncate(currentFileContent, 500);
}
float[] queryVector = embeddingModel.embed(queryText).content().vector();
List<VectorStore.SearchResult> codeContext = vectorStore.search(queryVector, 5, null);
// 2. 构建Prompt
String prompt = buildCodeAssistantPrompt(
question, currentFileContent, currentLanguage, codeContext);
// 3. 生成回答
return llm.generate(prompt);
}
/**
* 代码审查:检查代码是否符合内部规范
*/
public CodeReviewResult reviewCode(String code, String language) {
// 检索相关的规范文档
float[] queryVector = embeddingModel.embed("代码规范 最佳实践 " + language).content().vector();
List<VectorStore.SearchResult> conventions = vectorStore.search(queryVector, 3,
VectorStore.SearchFilter.builder()
.conditions(List.of(VectorStore.SearchFilter.FilterCondition.builder()
.field("type").value("coding_convention")
.operator(VectorStore.SearchFilter.FilterOperator.EQ).build()))
.build()
);
String conventionContext = conventions.stream()
.map(VectorStore.SearchResult::getContent)
.collect(Collectors.joining("\n\n"));
String prompt = """
你是一个代码审查专家,了解我们团队的内部编码规范。
内部规范参考:
%s
请审查以下代码,找出:
1. 违反内部规范的问题(HIGH优先级)
2. 潜在的bug或安全问题(HIGH优先级)
3. 性能问题(MEDIUM优先级)
4. 代码风格问题(LOW优先级)
代码:
```%s
%s
```
返回JSON:
{
"issues": [
{
"severity": "HIGH/MEDIUM/LOW",
"line": 行号或null,
"type": "问题类型",
"description": "问题描述",
"suggestion": "改进建议"
}
],
"summary": "总体评价",
"overallQuality": "GOOD/ACCEPTABLE/NEEDS_IMPROVEMENT"
}
只返回JSON。
""".formatted(conventionContext, language, code);
try {
String response = llm.generate(prompt);
// 解析JSON并返回
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(extractJson(response));
List<CodeIssue> issues = new ArrayList<>();
for (JsonNode issue : root.path("issues")) {
issues.add(new CodeIssue(
issue.path("severity").asText(),
issue.path("line").isNull() ? null : issue.path("line").asInt(),
issue.path("type").asText(),
issue.path("description").asText(),
issue.path("suggestion").asText()
));
}
return new CodeReviewResult(
issues,
root.path("summary").asText(),
root.path("overallQuality").asText()
);
} catch (Exception e) {
log.error("代码审查结果解析失败", e);
return new CodeReviewResult(List.of(), "审查失败,请重试", "UNKNOWN");
}
}
/**
* 代码生成:根据描述生成符合内部规范的代码
*/
public String generateCode(
String description, String language, String fileContext) {
// 检索类似的内部代码示例
float[] queryVector = embeddingModel.embed(description + " " + language)
.content().vector();
List<VectorStore.SearchResult> examples = vectorStore.search(queryVector, 3,
VectorStore.SearchFilter.builder()
.conditions(List.of(VectorStore.SearchFilter.FilterCondition.builder()
.field("type").value("usage_example")
.operator(VectorStore.SearchFilter.FilterOperator.EQ).build()))
.build()
);
String exampleContext = examples.isEmpty() ? "" :
"内部代码示例参考:\n" +
examples.stream().map(VectorStore.SearchResult::getContent)
.collect(Collectors.joining("\n\n"));
String fileContextSection = fileContext != null && !fileContext.isEmpty() ?
"当前文件上下文(用于理解类结构和导入):\n```" + language + "\n" +
truncate(fileContext, 500) + "\n```\n\n" : "";
String prompt = """
你是一个专业的%s工程师,了解我们团队的编码规范。
%s
%s
需求:%s
请生成符合需求和规范的代码,只返回代码,不要额外解释。
""".formatted(language, exampleContext, fileContextSection, description);
return llm.generate(prompt);
}
private String buildCodeAssistantPrompt(
String question, String fileContent,
String language, List<VectorStore.SearchResult> codeContext) {
String contextSection = codeContext.isEmpty() ? "" :
"内部代码库参考:\n" +
codeContext.stream().map(VectorStore.SearchResult::getContent)
.collect(Collectors.joining("\n\n---\n\n")) + "\n\n";
String fileSection = fileContent != null && !fileContent.isEmpty() ?
"当前文件内容:\n```" + language + "\n" + truncate(fileContent, 1000) + "\n```\n\n" : "";
return """
你是一个了解我们内部代码库的编程助手。
%s%s请回答以下问题,如果涉及代码示例,请使用我们内部框架的写法:
%s
""".formatted(contextSection, fileSection, question);
}
private String truncate(String s, int maxLen) {
return s.length() > maxLen ? s.substring(0, maxLen) + "..." : s;
}
private String extractJson(String s) {
int start = s.indexOf('{'); int end = s.lastIndexOf('}');
return (start >= 0 && end > start) ? s.substring(start, end + 1) : s;
}
record CodeIssue(String severity, Integer line, String type,
String description, String suggestion) {}
record CodeReviewResult(List<CodeIssue> issues, String summary, String overallQuality) {}
}实践建议
索引质量远比索引数量重要
把整个代码库都索引进去,反而会降低检索质量(太多噪声)。高价值的索引内容:公共API的方法签名+Javadoc、核心业务类的说明、使用示例(从测试代码提取)、架构决策记录(ADR文档)。低价值的内容:getter/setter、配置文件、工具类的实现细节。把索引精力集中在"工程师经常需要查阅"的内容上。
代码审查要针对团队的真实痛点
通用的代码审查规则网上一大堆,但团队专属的规范才是价值所在。在自己团队里做代码审查AI,要把团队的历史教训转化为规则:"不要直接访问业务数据库,必须通过Repository"、"所有对外接口必须有参数校验"、"异步任务必须有超时限制"。这些是通用AI不知道,但团队成员都懂的规则。
和IDE集成才是用户体验的关键
做了代码助手但要求工程师切到浏览器页面去问,使用率会很低。理想的集成方式:VS Code/IntelliJ插件,在IDE内直接调用。可以先用简单方案:一个JetBrains插件调用内部HTTP API,在侧边栏显示回答。这比一个完整的网页应用更容易被工程师日常使用。
