云计算百科
云计算领域专业知识百科平台

Spring Boot 3.3.4 集成 Atlassian Rovo API:企业知识库向 ...

Spring Boot 3.3.4 集成 Atlassian Rovo API:企业知识库向 LLM 执行引擎的端到端延迟治理

背景

上周有个需求,要把公司内部 Confluence 空间里散落的运维手册、故障排查 SOP、架构决策记录,通过 Atlassian Rovo 的检索能力喂给后端的 LLM 推理网关,让值班工程师在 Jira 工单里直接 @AI 拿到可执行的操作步骤。技术栈:Spring Boot 3.3.4、JDK 21.0.4、OpenAI GPT-4o-2024-11-20 作为推理引擎、Atlassian Rovo Search API(基于 Forge 平台)、Elasticsearch 8.15.1 做向量召回的兜底方案。

这个需求看起来只是「调个 API 拼个 Prompt」,但实际落地时,从 Confluence 页面检索到最终 LLM 返回结构化操作步骤,端到端 P99 延迟从预期的 2 秒飙到了 11.3 秒,值班工程师根本等不了。

检索层:Rovo Search API 的响应时间拆解

先说 Rovo Search API 的调用方式。它本质上是 Atlassian 在 Forge 平台上封装的统一检索接口,能跨 Jira、Confluence、Trello 做全文检索和语义检索。调用时需要用 OAuth 2.0 获取 access token,并通过 atlassian-announcement: read 等 scope 授权。

```java// Rovo Search 客户端封装 — Spring Boot 3.3.4@Servicepublic class RovoSearchClient {

private final RestClient restClient;private final String baseToken;

public RovoSearchClient(RestClient.Builder builder,@Value("${atlassian.rovo.base-url}") String baseUrl,@Value("${atlassian.rovo.api-token}") String token) {this.restClient = builder.baseUrl(baseUrl).build();this.baseToken = token;}

public List search(String query, int maxResults) {var request = RovoSearchRequest.builder().query(query).maxResults(maxResults).product("confluence").build();

var response = restClient.post().uri("/rest/api/search").header("Authorization", "Bearer " + baseToken).header("Content-Type", "application/json").body(request).retrieve().body(new ParameterizedTypeReference>() {});

示意图

return response != null ? response : Collections.emptyList();}}```

用 JMH 对 Rovo Search API 做了 1000 次基准测试,结果如下:

| 指标 | 值 ||——|——|| 平均响应时间 | 487ms || P50 | 412ms || P95 | 923ms || P99 | 1456ms || 最大响应时间 | 2831ms |

单次检索不算离谱,但问题出在检索结果的预处理环节。Rovo 返回的 Confluence 页面内容包含大量 ADF(Atlassian Document Format)格式的富文本,直接塞给 GPT-4o 会导致 token 消耗爆炸。

瓶颈定位:三层延迟叠加

用 async-profiler 3.0 做了火焰图分析,发现延迟集中在三个环节:

第一层:ADF 转 Markdown 的 CPU 密集操作。 Confluence 页面的 ADF 格式嵌套层级深,我们用递归解析器做转换,遇到包含大量表格和代码块的页面时,单次转换耗时可达 800-1200ms。这个操作在请求线程里同步执行,直接阻塞了后续流程。

第二层:Token 预算计算与截断策略。 GPT-4o-2024-11-20 的上下文窗口是 128K tokens,但企业知识问答场景下,我们只需要前 3 个最相关的 Confluence 片段。问题在于,如果不做精确的 token 计数,容易超预算触发 API 端的 400 错误。

第三层:LLM 推理本身的延迟。 这部分没法优化,只能靠并发控制来管理。

```java// 异步 ADF 转换 + Token 预算控制@Componentpublic class KnowledgePreprocessor {

private final ExecutorService adfExecutor;private final TokenCounter tokenCounter;

public KnowledgePreprocessor(@Value("${app.adf.executor.core-size:4}") int coreSize,TokenCounter tokenCounter) {this.adfExecutor = Executors.newFixedThreadPool(coreSize,Thread.ofVirtual().name("adf-convert-", 0).factory());this.tokenCounter = tokenCounter;}

public CompletableFuture> preProcessAsync(List results, int budgetTokens) {

var futures = results.stream().limit(3).map(r -> CompletableFuture.supplyAsync(() -> convertAdfToMarkdown(r.getContent()),adfExecutor)).toList();

return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenApply(v -> {List snippets = futures.stream().map(CompletableFuture::join).toList();

// 贪心填充:在 token 预算内尽可能多地保留片段List fitted = new ArrayList<>();int used = 0;for (String snippet : snippets) {int cost = tokenCounter.count(snippet);if (used + cost <= budgetTokens) {fitted.add(snippet);used += cost;}}return fitted;});}

private String convertAdfToMarkdown(JsonNode adf) {// ADF 递归解析逻辑,关键优化点:// 1. 用 Jackson 的 TreeTraverser 替代递归 walk,减少栈开销// 2. 表格转 Markdown 时跳过纯样式节点// 3. 代码块直接提取 text 节点,不处理 highlight 属性return AdfToMarkdownConverter.convert(adf);}}```

这里有个反直觉的发现:官方推荐的 ADF 解析方案是用 Atlassian 的 adf-java-parser 库,但在我们的场景下反而更慢——因为它的 AST 构建会保留所有节点属性,而我们的场景只需要纯文本。自己写了一个精简版解析器,用 Jackson 的 TreeTraverser 流式遍历,跳过了所有样式和元数据节点。

优化效果

| 指标 | 优化前 | 优化后 | 改善幅度 ||——|——–|——–|———-|| 端到端 P50 | 5.8s | 1.9s | -67.2% || 端到端 P95 | 8.7s | 3.2s | -63.2% || 端到端 P99 | 11.3s | 4.6s | -59.3% || ADF 转换 P95 | 1200ms | 340ms | -71.7% || 400 错误率 | 12.3% | 0.4% | -96.8% || Token 消耗(每次请求) | 平均 18,400 | 平均 6,200 | -66.3% |

示意图

用 JDK 21 的虚拟线程承载 ADF 转换任务后,线程池从固定 4 线程扩到了 32 虚拟线程,CPU 利用率从 34% 降到 21%——因为虚拟线程在 IO 等待时自动让出载体线程,不再占用平台线程资源。

踩坑记录

Rovo Search API 的 maxResults 参数上限是 50,但实际返回结果数经常少于请求数。原因是 Rovo 内部做了相关性过滤,低相关度的结果直接被丢弃。最初我们设 maxResults=10,结果发现 LLM 经常因为上下文不足给出泛泛的回答。调到 maxResults=20 后,虽然 token 消耗增加了,但答案的准确性和可操作性明显提升。

另一个坑是 OAuth token 的刷新。Atlassian 的 access token 有效期是 1 小时,但在高并发场景下,多个请求同时发现 token 过期,会触发「惊群刷新」,短时间内产生大量 refresh 请求。用 synchronized 保护刷新逻辑太粗暴,最终用了 StampedLock 的乐观读 + 悲观写模式解决。

这个方案虽然官方文档推荐直接用 RestTemplate 重试,但在我们场景下反而更糟——重试会放大 Rovo 侧的压力,导致整个检索链路雪崩。不如在本地做 token 缓存 + 提前刷新。

总结

企业知识库接入 LLM 的核心不在 Prompt 工程,而在检索结果的预处理管线。ADF 解析、Token 预算控制、异步化执行这三个环节决定了端到端延迟的上限。用虚拟线程替代固定线程池处理 CPU 密集的格式转换,配合贪心 token 填充策略,P99 从 11.3 秒压到了 4.6 秒,值班工程师终于愿意等 AI 的回答了。

#后端 #Java #SpringBoot #性能优化 #Atlassian


你在实际项目中有遇到类似问题吗?欢迎在评论区分享你的经验和解决方案。

赞(0)
未经允许不得转载:网硕互联帮助中心 » Spring Boot 3.3.4 集成 Atlassian Rovo API:企业知识库向 ...
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!