- 环境准备
- 自动埋点接入(Agent 方式)
- application.yml 配置
- 手动埋点(@Trace / ActiveSpan)
- 大模型调用链路追踪实战
- 常见问题排查
1. 环境准备
本篇在 Spring Boot 3.x 工程上,演示从零接入 SkyWalking,并针对大模型(OpenAI 风格 / 自研 LLM 网关)调用做链路追踪。
1.1 版本匹配
组件 | 版本建议 | 说明 |
--- | --- | --- |
Spring Boot | 3.2.x | JDK 17 |
SkyWalking | 9.7+ | Agent 与 OAP 同版本 |
OAP + UI | 9.7+ | 独立部署 |
Storage | Elasticsearch 8.x | 或 BanyanDB |
1.2 目录规划
project/
├── docker-compose.yml # OAP + ES + UI
├── agent/
│ └── skywalking-agent.jar # 探针
└── src/main/resources/
└── application.yml
2. 自动埋点接入(Agent 方式)
自动埋点无需改业务代码,仅需在启动时挂载 Agent。SkyWalking 已内置对 Spring MVC、OpenFeign、RestTemplate、JDBC、Redis、Kafka 等组件的插件。
2.1 启动脚本挂载 Agent
java -javaagent:/opt/agent/skywalking-agent.jar \
-Dskywalking.agent.service_name=chat-service \
-Dskywalking.collector.backend_service=oap:11800 \
-jar chat-service.jar
2.2 通过 IDEA / Maven 配置
在 `pom.xml` 中约定 agent 路径,也可在启动参数里直接写:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>
-javaagent:${project.basedir}/agent/skywalking-agent.jar
-Dskywalking.agent.service_name=chat-service
-Dskywalking.collector.backend_service=oap:11800
</jvmArguments>
</configuration>
</plugin>
启动后,访问一次接口,去 SkyWalking UI 的「拓扑」即可看到 `chat-service` 节点与其下游依赖。
3. application.yml 配置
除启动参数外,部分行为建议落到配置文件(或 `agent.config`)中统一管理。
# application.yml(业务侧仅声明自身配置)
spring:
application:
name: chat-service
datasource:
url: jdbc:mysql://mysql:3306/chat
redis:
host: redis
# SkyWalking 推荐通过 agent.config 控制,关键项示例:
# agent.service_name=${SW_AGENT_NAME:chat-service}
# collector.backend_service=${SW_BACKEND:oap:11800}
# agent.sample_rate=10000
# plugin.toolkit.log.grpc.reporter.server_host=oap
> 最佳实践:启动参数用于"环境相关"的地址/服务名,`agent.config` 用于"策略相关"的采样率/忽略后缀,二者配合,避免硬编码。
4. 手动埋点(@Trace / ActiveSpan)
自动埋点覆盖通用框架,但**大模型调用、内部业务片段**需要用手动埋点补充,否则链路会"断"或"粗"。
4.1 @Trace 注解
SkyWalking 提供 `org.apache.skywalking.apm.toolkit.trace.Trace` 注解,标注的方法会被当作一个 Local Span。
import org.apache.skywalking.apm.toolkit.trace.Trace;
import org.apache.skywalking.apm.toolkit.trace.Tag;
@Service
public class ChatService {
@Trace(operationName = "buildPrompt")
@Tag(key = "prompt.length", value = "arg[0].length()")
public String buildPrompt(String userQuery) {
// 组装提示词,作为独立 Local Span 便于定位耗时
return PromptTemplate.render(userQuery);
}
}
4.2 ActiveSpan 手动打 Tag / Log
在方法内部可获取当前 Span,附加自定义标签(如大模型参数、Token 数):
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
public CompletionResult callLLM(String model, String prompt) {
ActiveSpan.tag("llm.model", model);
ActiveSpan.tag("llm.prompt.length", String.valueOf(prompt.length()));
long start = System.currentTimeMillis();
CompletionResult result = llmClient.complete(model, prompt);
ActiveSpan.tag("llm.token.prompt", String.valueOf(result.promptTokens()));
ActiveSpan.tag("llm.token.completion", String.valueOf(result.completionTokens()));
ActiveSpan.tag("llm.cost.usd", result.costUsd().toString());
ActiveSpan.tag("llm.ttft.ms", String.valueOf(result.timeToFirstTokenMs()));
return result;
}
4.3 跨线程上下文传播
异步调用会丢失上下文,需用 SkyWalking 提供的包装器:
import org.apache.skywalking.apm.toolkit.trace.RunnableWrapper;
CompletableFuture.supplyAsync(RunnableWrapper.of(() -> {
// 此处的 Span 上下文与父线程一致
return callLLM(model, prompt);
}));
5. 大模型调用链路追踪实战
下面用一个完整示例,把「HTTP 调用 LLM 网关」纳入链路,并采集 Token / 时延 Tag。
5.1 自定义 LLM Exit Span 拦截器(插件式增强)
对于 SkyWalking 未内置的 LLM SDK,建议写一个自动插件(参考第 03 篇),在 SDK 的 `complete` 方法上织入 Exit Span:
// 简化:手动方式在业务层包一层 ExitSpan
public CompletionResult tracedComplete(String model, String prompt) {
// 标记为 Exit:跨进程访问远端 LLM
AbstractSpan span = ContextManager.createExitSpan(
"LLM/" + model, new ContextCarrier(), "llm-gateway:443");
try {
ActiveSpan.tag("llm.model", model);
CompletionResult r = llmClient.complete(model, prompt);
ActiveSpan.tag("llm.token.prompt", String.valueOf(r.promptTokens()));
ActiveSpan.tag("llm.ttft.ms", String.valueOf(r.timeToFirstTokenMs()));
return r;
} catch (Exception e) {
ActiveSpan.error(e); // 标记 Span 异常
throw e;
} finally {
ContextManager.stopSpan();
}
}
5.2 Controller 串联整条链路
@RestController
@RequestMapping("/chat")
public class ChatController {
@GetMapping("/ask")
public Map<String, Object> ask(@RequestParam String q) {
// EntrySpan 由 Spring MVC 插件自动创建
String prompt = chatService.buildPrompt(q); // @Trace Local Span
List<Doc> docs = vectorService.search(q); // 自动 Exit Span(MySQL/向量)
CompletionResult r = llmGateway.tracedComplete("gpt-4o", prompt); // 手动 Exit Span
Map<String, Object> resp = new HashMap<>();
resp.put("answer", r.text());
resp.put("tokens", r.promptTokens() + r.completionTokens());
return resp;
}
}
此时一次 `/chat/ask` 调用在 UI 中呈现的 Span 树见图 figure_04_3:Entry → buildPrompt(Local) → vectorSearch(Exit) → LLM/Exit,各段时延与 Tag 一目了然。
6. 常见问题排查
6.1 拓扑看不到服务
现象 | 可能原因 | 处理 |
--- | --- | --- |
服务不显示 | Agent 未挂载 / backend_service 错误 | 检查 `-javaagent` 与地址 |
无 Trace | 采样率过低 | 调高 `agent.sample_rate` |
链路断点 | 跨进程未传播 | 确认网关/SDK 透传 sw8 头 |
6.2 链路断在异步/线程池
现象:下游服务的 Span 不在同一 Trace。原因多为线程切换丢失上下文。
// 错误:直接 new Thread,上下文丢失
new Thread(() -> callLLM(...)).start();
// 正确:使用 RunnableWrapper 传播上下文
new Thread(RunnableWrapper.of(() -> callLLM(...))).start();
6.3 大模型链路被采样丢弃
大模型调用慢且贵,必须排除采样。在 `agent.config`:
agent.sample_rate=10000
# 对慢端点强制记录(OAP 侧 slowTraceThreshold 配置)
6.4 Agent 启动报错 ClassCircularityError
多为插件冲突,精简 `plugins/` 目录,只保留实际使用的插件(如只留 `spring-mvc`、`httpclient`、`jdbc`、`redis`)。
小结
本篇落地了 Spring Boot 接入 SkyWalking 的完整路径:Agent 自动埋点零侵入接入通用组件;`@Trace` 与 `ActiveSpan` 补充业务与大模型段埋点;通过 `RunnableWrapper` 解决异步上下文传播;并给出链路断点、采样、插件冲突等常见坑的排查表。下一篇我们将系统规范大模型链路的 Trace/Span 建模与 Tag 命名,让追踪数据真正可分析。