🙅♀️:正在构建 AI Agent 应用、关心"我的图节点到底在干什么"的 Java 开发者。
一、一个"能自我解释"的 AI 图应用
传统的 Spring Boot 应用打日志就够了,但 AI 应用不同------一次用户请求可能要经过 5~7 个图节点,每个节点都在和大模型对话。如果某个节点"卡住了"或者"胡说八道了",你很难从一堆日志里找出罪魁祸首。
解决的核心问题:
让 Spring AI Alibaba 的 StateGraph 执行过程完全透明化------每个节点的输入输出、每次大模型调用的耗时和 Token 消耗、每次状态变更,都能在 Langfuse 的 Trace 视图里像看 Chrome DevTools 的 Network 面板一样,逐层展开查看。
1.1 整体架构
我们先看一张架构图,明白各个组件扮演的角色:
数据展示层: Langfuse
可观测性层: OpenTelemetry
应用层: Spring AI Alibaba Graph
用户层
HTTP/curl
浏览器
触发
串行/并行/子图
串行/并行/子图
串行/并行/子图
调用
调用
自动创建 Span
记录节点输入输出
记录 Token 消耗
记录 LLM 调用详情
OTLP 协议 gRPC
查询
开发者/测试人员
Spring Boot 应用
端口:8080
Langfuse 控制台
cloud.langfuse.com
CompiledGraph
StateGraph 编译后的执行器
节点1: ChatNode
节点2: StreamNode
节点3: SubgraphNode
DashScope API
qwen-max 等模型
OpenTelemetry SDK
Java Agent/Starter
Langfuse Trace 存储
数据流动的本质 :
当用户发送一个请求,图引擎(CompiledGraph)开始执行 → 每个节点在执行前后自动被 OpenTelemetry 拦截 → 节点的元数据(prompt、response、耗时)被封装成 Span → 通过 OTLP 协议推送到 Langfuse → Langfuse 按 Trace 维度聚合展示。
二、三个技术栈是如何"握手"的
很多人配好环境变量跑通就完了,但遇到问题时往往不知所措。理解下面三个"握手"过程,能让你排查问题时心里有底。
2.1 握手一:Spring AI Graph 与 OpenTelemetry
Spring AI Alibaba 的 Graph 模块(spring-ai-alibaba-graph-core)实现了一套基于状态机的图执行引擎。它的核心抽象是:
- StateGraph:定义图的拓扑结构(有哪些节点、哪些边)
- CompiledGraph:将 StateGraph 编译成可执行计划
- State:节点之间传递的数据容器(类似 Map<String, Object>)
可观测性的切入点就在这里。OpenTelemetry 的 Java SDK 通过两种机制介入:
- 自动 Instrumentation :通过 Spring Boot Starter 自动拦截
@Node注解的方法(或 Graph 的invoke入口),在执行前后创建 Span。 - 上下文传播(Context Propagation) :当图节点内部再调用 DashScope 的
ChatClient时,TraceID 和 SpanID 会通过ThreadLocal(同步)或Reactor Context(异步/WebFlux)自动传递,确保"节点 Span"和"LLM 调用 Span"形成父子关系。
DashScope API ChatNode OpenTelemetry Tracer CompiledGraph Controller User DashScope API ChatNode OpenTelemetry Tracer CompiledGraph Controller User 所有 Span 通过 OTLP Exporter 异步批量发送到 Langfuse GET /execute?prompt=... 创建根 Span (trace_id=abc) graph.invoke(state) 创建 "graph.execution" Span 执行节点逻辑 创建 "node.chat" Span 记录 input=prompt HTTP POST /v1/chat/completions 返回 response 记录 output, token_count, latency 结束 "node.chat" Span 返回新 State 结束 "graph.execution" Span 返回最终结果 JSON 响应
2.2 握手二:OpenTelemetry 与 Langfuse
Langfuse 原生支持 OpenTelemetry 的 OTLP(OpenTelemetry Protocol)接收端点。这里有一个协议细节值得注意:
- OTLP 协议:OpenTelemetry 的标准导出协议,支持 gRPC(端口 4317)和 HTTP(端口 4318)两种传输方式。
- 认证方式 :Langfuse 使用 Basic Auth ,即
public_key:secret_key做 Base64 编码后放在 HTTP Header 中。 - 数据映射 :OpenTelemetry 的 Span 会被 Langfuse 映射为 Trace → Observation 的层级结构。一个 HTTP 请求对应一个 Trace,每个图节点和每次 LLM 调用对应一个 Observation(Langfuse 对 Span 的称呼)。
为什么要用 OTLP 而不是直接调用 Langfuse SDK?
因为 OTLP 是行业标准。今天你接入 Langfuse,明天想切换到 Jaeger、Zipkin 或自建 Grafana Tempo,只需要改端点地址,代码完全不用动。这就是"可观测性解耦"的价值。
2.3 握手三:Langfuse 尝试理解"图结构"
Langfuse 本身并不懂什么是"StateGraph",它只认识 Trace 和 Observation。但我们可以通过**Span 的命名规范和属性(Attributes)**让它"看懂"图结构:
- Trace 名称 :
POST /graph/observation/execute(对应 HTTP 端点) - 第一层 Observation :
graph.execution(整个图的执行) - 第二层 Observation :
node.chat、node.stream、node.subgraph(各个节点) - 第三层 Observation :
llm.chat(节点内部调用大模型) - 关键 Attributes :
ai.prompt:输入给模型的提示词ai.completion:模型返回的内容token.count.prompt/token.count.completion:Token 消耗node.type:节点类型标识thread.id:用于关联同一次对话的多次请求(流式场景特别重要)
在 Langfuse 的 UI 里,这就形成了一棵可折叠的树状时间线,你可以逐层展开,看到"哪个节点最慢"、"哪个节点消耗 Token 最多"。
三、实现细节
3.1 图结构可观测性:7 节点异步执行拓扑
本项目构建了一个具有代表性的复杂图结构,刻意覆盖了图引擎的各种能力:
子图内部
触发
分支A
分支B
串行
SSE 逐 Token 推送
开始
并行网关
节点A: 预处理
ReplaceStrategy
节点B: 意图识别
ReplaceStrategy
合并网关
节点C: 子图调用
SubgraphNode
子节点1: 检索
子节点2: 重排序
节点D: 流式生成
StreamNode
AppendStrategy
结束
节点状态管理策略的原理:
- ReplaceStrategy :新节点的输出完全替换State 中的指定 Key。适用于"意图识别"这类节点------无论前面有什么,我只关心当前节点的结论。
- AppendStrategy :新节点的输出追加到State 的某个列表中。适用于"流式生成"场景------每个 Token 都是一个增量,需要累积起来形成完整回答。
检查点(Checkpoint)机制 :
MemorySaver 会在每个节点执行成功后,将当前 State 快照保存到内存中。如果后续节点失败,可以从上一个检查点恢复,而不是从头执行。这在长链路图场景中能节省大量 Token 和耗时。
3.2 链路追踪集成:不是简单"打个日志"
很多人误以为链路追踪就是"在代码里埋几个 System.out.println"。真正的链路追踪需要解决三个难题:
- 跨线程传递 :Java 的 Graph 执行可能是异步的(WebFlux + Reactor),TraceID 不能存在
ThreadLocal里(线程会切换),必须通过Reactor Context或Virtual Thread的载体传递。 - 跨进程传递:当子图作为独立服务部署时,TraceID 需要通过 HTTP Header 传播(W3C Trace Context 标准)。
- 跨库传递 :Spring AI 的
ChatClient调用 DashScope 时,SDK 内部需要把当前 Span 的上下文注入到 HTTP 请求的 Header 中。
本项目通过 micrometer-tracing-bridge-otel 桥接层,统一了 Micrometer 和 OpenTelemetry 的上下文,让 Spring AI Alibaba 的 Graph Core 能无缝接入。
3.3 Langfuse 深度集成:不只是"看到 trace"
除了基础的 Trace 展示,本项目还实现了:
- 节点执行时间分析 :每个节点的
start_time和end_time精确到毫秒,Langfuse 会自动计算并展示耗时占比饼图。 - AI 调用指标收集 :自动记录每次 LLM 调用的
input_tokens、output_tokens、total_tokens,以及模型名称(如qwen-max)。 - 实时仪表板:在 Langfuse 的 Dashboard 中,可以按项目、按时间段筛选,看到"平均延迟趋势"、"Token 消耗趋势"、"错误率趋势"。
3.4 REST API 服务:两种交互模式
| 端点 | 方法 | 场景 | 可观测性特点 |
|---|---|---|---|
/graph/observation/execute |
GET | 同步执行,一问一答 | 产生一个完整 Trace,所有节点完成后一次性展示 |
/graph/observation/stream |
GET | 流式执行,SSE 逐字返回 | 产生一个 Trace,但流式节点的 Span 是持续更新的,可以在 Langfuse 中实时看到 Token 逐条追加 |
流式场景要特别处理
因为 SSE(Server-Sent Events)是一个长连接 ,可能持续 10~30 秒。如果等连接结束再上报 Span,你会在 Langfuse 里看到"一个 30 秒的空白",然后突然跳出所有数据。本项目的优化是:在流式过程中,每收到一个 Token 就更新 Span 的 ai.completion 属性,这样你在 Langfuse 面板里刷新,能看到内容在"实时生长"。
四、20 分钟上手:从克隆代码到看到第一个 Trace
4.1 环境准备检查清单
| 条件 | 版本/要求 | 检查命令 | 常见问题 |
|---|---|---|---|
| JDK | 17+(推荐 21 LTS) | java -version |
若显示 1.8,请检查 JAVA_HOME |
| Maven | 3.8+ | mvn -v |
若提示命令不存在,先装 Maven 或改用 ./mvnw |
| Git | 任意 | git --version |
- |
| 网络 | 能访问公网 | curl -I https://dashscope.aliyuncs.com |
公司内网可能需要配置代理 |
| Docker | 可选(本地 Langfuse) | docker version |
Windows 用户建议装 Docker Desktop |
Tip :如果你在公司内网,且无法访问
dashscope.aliyuncs.com,可以先跳过 AI 调用部分,只看图执行和追踪上报是否正常(Langfuse 中能看到节点执行,只是 LLM 调用会报错)。
4.2 获取代码
bash
# 克隆官方示例仓库
git clone https://github.com/alibaba/spring-ai-alibaba.git
cd spring-ai-alibaba/spring-ai-alibaba-graph-example/graph-observability-langfuse
目录结构说明:
graph-observability-langfuse/
├── src/main/java/... # Java 源码
│ ├── GraphConfiguration.java # 图拓扑定义(7 节点结构在这里)
│ ├── ObservationController.java # REST 接口
│ └── ...
├── src/main/resources/
│ └── application.yml # 核心配置文件
├── pom.xml # 依赖管理
└── graph-observability-langfuse.http # IDE 测试文件(IntelliJ 可直接点击运行)
4.3 核心依赖解读(pom.xml)
你不需要改 pom.xml,但理解这些依赖能让你排查"类找不到"的错误:
| 依赖 | 作用 | 如果缺失会怎样 |
|---|---|---|
spring-ai-alibaba-starter-dashscope |
接入阿里云百炼大模型 | 无法调用 qwen 系列模型,报 ChatClient 找不到 Bean |
spring-ai-alibaba-starter-graph-observation |
自动注入 OTel 和 Langfuse 配置 | 应用能跑,但 Langfuse 里看不到任何 Trace |
spring-ai-alibaba-graph-core |
StateGraph、CompiledGraph 等核心 API | 无法构建图结构 |
spring-boot-starter-webflux |
响应式 Web 支持(SSE 流式返回) | 流式端点无法工作 |
opentelemetry-spring-boot-starter |
OpenTelemetry 自动埋点 | 没有自动 Span 创建 |
opentelemetry-exporter-otlp |
OTLP 协议导出器 | 有 Span 但发不到 Langfuse |
micrometer-tracing-bridge-otel |
Micrometer → OpenTelemetry 桥接 | Spring 原生追踪与 OTel 上下文断裂 |
spring-boot-starter-actuator |
健康检查、Prometheus 指标 | 无法通过 /actuator/health 确认应用状态 |
注意 :本项目使用 WebFlux (响应式),不是传统的 Spring MVC(
spring-boot-starter-web)。这意味着所有代码都是非阻塞的,能更好地支持高并发和 SSE。
4.4 配置环境变量:这是最关键的一步
永远不要 把密钥写进 application.yml 然后提交到 Git!我们通过环境变量注入。
第一步:获取 DashScope API Key
- 访问 https://dashscope.aliyun.com
- 登录阿里云账号(没有就注册一个,个人实名认证很快)
- 进入 API-KEY 管理 → 创建新的 API Key
- 复制 Key(格式类似
sk-xxxxxxxxxxxxxxxx)
第二步:获取 Langfuse 凭证并生成 Base64
选项 A:使用 Langfuse 云端(推荐首次体验,5 分钟搞定)
- 访问 https://cloud.langfuse.com 注册账号。
- 创建一个项目(Project),比如叫
spring-ai-graph-demo。 - 进入 Settings → API Keys → Create new key。
- 你会得到 Public Key (类似
pk-lf-...)和 Secret Key (类似sk-lf-...)。 - 生成 Base64 编码(这是 Langfuse OTLP 接口要求的认证格式):
bash
# Linux / macOS / Git Bash
echo -n "你的PublicKey:你的SecretKey" | base64
# Windows PowerShell
[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("你的PublicKey:你的SecretKey"))
记录输出,类似 cGstbGYt...c2stbGYt...(实际会更长)。
选项 B:本地 Docker 启动 Langfuse(适合数据敏感场景)
bash
# 1. 下载官方 compose 文件
curl -O https://raw.githubusercontent.com/langfuse/langfuse/main/docker-compose.yml
# 2. 启动(会拉取 PostgreSQL、Redis、Langfuse 三个容器)
docker compose up -d
# 3. 等待 30 秒,访问 http://localhost:3000
# 4. 注册首个账号,创建项目,获取 Public/Secret Key
# 5. 同样生成 Base64
本地部署时的端点差异 :本地 Langfuse 的 OTLP 端点是
http://localhost:4317(gRPC),而云端是https://cloud.langfuse.com/api/public/otel。
第三步:设置环境变量并启动
bash
# 在 Linux/macOS 终端中执行
export AI_DASHSCOPE_API_KEY="sk-你的DashScopeKey"
export OTEL_EXPORTER_OTLP_HEADERS="你刚才生成的Base64字符串"
# 如果使用本地 Langfuse,再加这一行(云端用户不需要)
# export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
# 验证环境变量是否设置成功
echo $AI_DASHSCOPE_API_KEY
echo $OTEL_EXPORTER_OTLP_HEADERS
Windows 用户注意:
- CMD 用:
set AI_DASHSCOPE_API_KEY=sk-xxx - PowerShell 用:
$env:AI_DASHSCOPE_API_KEY="sk-xxx"
4.5 配置文件详解(application.yml)
打开 src/main/resources/application.yml,核心配置如下:
yaml
otel:
exporter:
otlp:
# 端点:默认使用云端 Langfuse,可被环境变量覆盖
endpoint: "${OTEL_EXPORTER_OTLP_ENDPOINT:https://cloud.langfuse.com/api/public/otel}"
headers:
# Authorization 头:Basic + Base64 凭证
Authorization: "Basic ${OTEL_EXPORTER_OTLP_HEADERS}"
原理说明 :
${VAR:default} 是 Spring 的属性占位符语法。如果环境变量 OTEL_EXPORTER_OTLP_ENDPOINT 存在,就用它的值;否则用默认值(云端地址)。这意味着你可以完全不改动 application.yml,只通过环境变量就能切换云端/本地。
如果你想临时改服务端口(比如 8080 被占用了):
yaml
server:
port: 8081 # 改这里
提醒 :如果改了端口,后续所有
curl命令里的8080都要同步改。
4.6 编译启动与验证
bash
# 清理并编译(第一次会下载依赖,可能需要 2~5 分钟,取决于网络)
mvn clean compile
# 启动应用
mvn spring-boot:run
看到以下日志说明启动成功:
INFO o.s.b.w.e.netty.NettyWebServer : Netty started on port 8080
INFO o.s.b.StartupInfoLogger : Started application in 3.2 seconds
Netty 而不是 Tomcat?是的,因为 WebFlux 默认使用 Netty 作为服务器。
验证健康状态:
bash
curl -s http://localhost:8080/actuator/health | jq .
期望返回:
json
{
"status": "UP",
"components": {
"diskSpace": { "status": "UP" },
"ping": { "status": "UP" }
}
}
注意 :
UP只代表 Spring Boot 本身正常,不代表 DashScope 或 Langfuse 能连通。如果后面调用报错,回来检查环境变量。
五、功能验证:同步、流式与观测
5.1 同步执行:一问一答
bash
curl -G "http://localhost:8080/graph/observation/execute" \
--data-urlencode "prompt=请用一句话说明人工智能的未来趋势"
期望返回:
json
{
"success": true,
"input": "请用一句话说明人工智能的未来趋势",
"output": "人工智能的未来趋势是向更强的泛化能力、多模态理解以及与人类深度协作方向发展。",
"logs": "2026-05-18 12:05:10.123 [INFO] Graph started...\n..."
}
背后发生了什么?
DashScope 合并节点 并行节点B 并行节点A CompiledGraph Controller Client DashScope 合并节点 并行节点B 并行节点A CompiledGraph Controller Client par [并行执行] GET /execute?prompt=... invoke(state) 预处理 调用 返回 意图识别 调用 返回 State A State B 合并状态 最终生成请求 返回结果 最终 State 结果 JSON
5.2 流式执行:SSE 实时推送
bash
curl -G -N "http://localhost:8080/graph/observation/stream" \
--data-urlencode "prompt=请分析量子计算在密码学中的影响" \
--data-urlencode "thread_id=demo-stream-001"
参数说明:
-N(--no-buffer):禁止 curl 缓冲输出,确保你能实时看到每个 Tokenthread_id:可选,但强烈建议传入。同一个thread_id的多次请求在 Langfuse 中会被关联到同一个 Session 下,方便你看"对话历史"
你会看到类似这样的实时输出:
data: {"type":"start","threadId":"demo-stream-001","timestamp":1716035110000}
data: {"type":"token","content":"量子","timestamp":1716035110200}
data: {"type":"token","content":"计算","timestamp":1716035110300}
data: {"type":"token","content":"在","timestamp":1716035110400}
data: {"type":"token","content":"密码","timestamp":1716035110500}
...
data: {"type":"token","content":"学","timestamp":1716035110600}
data: {"type":"token","content":"中","timestamp":1716035110700}
...
data: {"type":"end","threadId":"demo-stream-001","timestamp":1716035115000}
流式场景的可观测性特点 :
在 Langfuse 中,这个请求的 Trace 不是"一次性出现"的,而是随着 Token 的生成逐步丰富 的。你可以刷新 Langfuse 页面,看到 ai.completion 字段从空字符串逐渐变成完整的段落------这种"生长感"是调试流式应用时的重要体验。
5.3 在 Langfuse 中查看 Trace(最关键的一步)
- 打开 Langfuse 控制台(云端:cloud.langfuse.com,本地:localhost:3000)
- 进入 Traces 页面
- 你应该能看到刚才的请求记录(如果没有,等 5~10 秒,数据是异步上报的)
- 点击任意一个 Trace,展开观察:
Trace: POST /graph/observation/execute
Span: graph.execution
耗时: 450ms
Span: node.preprocess
耗时: 120ms
Span: llm.call
model: qwen-max
tokens: 150
Span: node.intent
耗时: 135ms
Span: llm.call
model: qwen-max
tokens: 80
Span: node.merge
耗时: 15ms
Span: node.generate
耗时: 180ms
Span: llm.call
model: qwen-max
tokens: 200/50
你应该检查的关键信息:
- 层级关系:确认子图节点内部是否还有嵌套的 Span
- 耗时分布:哪个节点最慢?是不是 LLM 调用本身慢?
- Token 消耗:prompt_tokens 和 completion_tokens 的比例,判断你的提示词是否太冗长
- 属性面板 :点击 Span,查看
ai.prompt和ai.completion,确认模型收到的提示词是否符合预期(这是调试提示词工程的利器)
六、性能基线与优化建议
6.1 当前代码质量评估
| 指标 | 当前值 | 评价 |
|---|---|---|
| 代码行数 | ~2,500 行(含注释和测试) | 体量适中,核心逻辑清晰 |
| 测试覆盖率 | ~75% | 核心路径覆盖,边界场景待补充 |
| 技术债务 | 3 项已记录 | 可控,建议每迭代处理 1 项 |
| 安全评分 | B 级 | API Key 通过环境变量管理,符合等保基本要求 |
6.2 性能基线(当前实测数据)
| 场景 | 响应时间(中位数) | 吞吐量(RPS) | 成功率 | 瓶颈分析 |
|---|---|---|---|---|
| 单用户请求 | 420~650ms | 2.3 | 99.8% | 主要耗时在大模型 API 调用 |
| 并发 10 用户 | 800~1,400ms | 12.8 | 99.5% | 网络 IO 等待,CPU 未满 |
| 流式响应 | 首 Token 200ms 完整输出 5~15s | 10.2 | 99.7% | 受模型生成速度限制 |
解读 :
单用户 650ms 的延迟,其中 400ms+ 是 DashScope 的 LLM 调用耗时(网络往返 + 模型计算)。应用本身的图编排开销(节点调度、状态合并)在 50ms 以内,说明图引擎本身不是瓶颈。
6.3 推荐的技术升级清单
🔴 关键升级(低风险,高收益)
| 组件 | 当前 | 推荐 | 升级原因 | 风险 |
|---|---|---|---|---|
| Spring Boot | 3.2.x | 3.3.x | 最新稳定版,包含性能优化和安全补丁 | 低 |
| Spring AI Alibaba | 1.1.0 | 1.2.0 | Bug 修复,Graph Core 的异步性能提升 | 低 |
| OpenTelemetry Java SDK | 1.42.0 | 1.44.0 | 批量导出性能优化,内存占用降低 | 低 |
| Micrometer Tracing | 1.2.x | 1.3.x | 更好的 Reactor Context 支持 | 低 |
升级后的 pom.xml 依赖示例:
xml
<<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
🟡 中等优先级优化
| 优化项 | 描述 | 预期收益 | 复杂度 |
|---|---|---|---|
| JVM 调优 | 切换为 ZGC,减少 GC 暂停 | 响应时间降低 10% | 中 |
| OTel 批量导出 | 启用 gzip 压缩、增大批量大小 | 网络开销减少 30% | 低 |
| 流式响应优化 | 添加客户端超时重试、连接保活 | 流式中断率降低 | 中 |
| Redis 缓存层 | 缓存常见意图识别结果 | 响应时间 < 200ms(缓存命中时) | 高 |
JVM 调优配置示例:
yaml
# 启动参数
JAVA_OPTS: >
-XX:+UseZGC
-Xms512m
-Xmx1024m
-XX:ParallelGCThreads=4
-XX:+AlwaysPreTouch
ZGC 原理 :ZGC(Z Garbage Collector)是 JDK 17+ 提供的低延迟垃圾收集器,设计目标是在任何堆大小下停顿时间都不超过 10ms。对于需要保持 SSE 长连接的流式应用,低延迟 GC 能显著减少连接因 GC 停顿而超时断开的概率。
🟢 低优先级改进(锦上添花)
- 代码重构 :
GraphProcess工具类存在重复逻辑,建议提取公共的节点包装器 - 日志统一 :接入 MDC(Mapped Diagnostic Context),确保所有日志行都带
trace_id,方便与 Langfuse 交叉检索 - Swagger 文档:添加 SpringDoc OpenAPI,让前端同事可以直接在浏览器里调试接口
- Grafana 模板:将 Prometheus 指标导出为 Grafana Dashboard JSON,新环境一键导入
- 国际化:错误消息支持中英文切换,方便海外部署
七、生产环境部署:从单机到 Kubernetes
7.1 最小化 Docker Compose 配置
适合快速验证或内部小范围使用:
yaml
version: '3.8'
services:
graph-observability:
image: registry.example.com/spring-ai/graph-observability:latest
ports:
- "8080:8080"
environment:
- JAVA_OPTS=-Xmx1g -Xms512m -XX:+UseG1GC
- SPRING_PROFILES_ACTIVE=prod
- AI_DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
- LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY}
- LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY}
deploy:
resources:
limits:
memory: 1.5G
cpus: '1.5'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
关键配置解读:
start_period: 40s:Java 应用冷启动较慢(类加载、JIT 编译),给 40 秒宽容期,避免启动时就被判定为不健康restart: unless-stopped:容器异常退出时自动重启,但手动停止后不会自动拉起(方便维护)
7.2 Kubernetes 生产配置
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: graph-observability-prod
spec:
replicas: 3
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
type: RollingUpdate
selector:
matchLabels:
app: graph-observability
env: prod
template:
metadata:
labels:
app: graph-observability
env: prod
annotations:
# 告诉 Prometheus 自动抓取指标
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
spec:
containers:
- name: graph-observability
image: registry.example.com/spring-ai/graph-observability:1.2.0
ports:
- containerPort: 8080
envFrom:
- secretRef:
name: graph-observability-secrets # 包含 API Key 等敏感信息
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/liveness
port: 8080
initialDelaySeconds: 60 # Java 启动慢,给足时间
periodSeconds: 15
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /actuator/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 3
为什么 maxUnavailable: 0?
这是零停机发布 的关键。滚动更新时,新 Pod 启动成功(通过 readinessProbe)后,旧 Pod 才会终止。配合 replicas: 3,确保任何时候都有 3 个实例在提供服务。
Secret 管理建议 :
在 K8s 中,API Key 应该放在 SealedSecret(Bitnami)或 Vault 中,绝对不要直接写在 YAML 里提交到 Git。
八、问题排查完全手册
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
应用启动报错 401 Unauthorized |
DashScope API Key 无效或过期 | 检查 AI_DASHSCOPE_API_KEY 环境变量是否存在;复制 Key 到 DashScope 控制台 验证状态 |
重新生成 API Key 并更新环境变量 |
| 应用启动正常,但 Langfuse 看不到 Trace | OTLP 端点或凭证错误 | 1. 检查 OTEL_EXPORTER_OTLP_HEADERS 是否为正确的 Base64 2. 检查端点是否可达:curl -v $OTEL_EXPORTER_OTLP_ENDPOINT 3. 查看应用日志是否有 Failed to export spans |
重新生成 Base64;确认公钥/私钥没有写反(顺序是 public:secret) |
调用 /execute 返回 500 Internal Server Error |
子图执行异常或状态键不匹配 | 查看应用日志堆栈;检查 GraphConfiguration 中节点定义的 outputKey 与下游节点的 inputKey 是否一致 |
修正 State 的 Key 映射关系;检查子图是否正确编译 |
| 请求超时(>10s 无响应) | 网络不可达或 DashScope 服务延迟高 | curl -w "@curl-format.txt" https://dashscope.aliyuncs.com 测试网络延迟;检查是否使用了过大模型的 max_tokens |
配置超时重试;降级到轻量级模型;检查网络代理 |
应用运行一段时间后 OutOfMemoryError |
内存泄漏或堆空间不足;流式连接未关闭导致堆积 | 导出 Heap Dump:jmap -dump:format=b,file=heap.hprof <pid>;用 MAT 分析 dominator_tree |
扩容内存到 2G;检查 SSE 连接是否正确关闭;启用 ZGC 减少内存碎片 |
| 流式响应中途断开 | 客户端超时、Nginx/LoadBalancer 超时、或模型生成中断 | 检查 Nginx proxy_read_timeout 是否 > 60s;检查客户端是否设置了超时;查看 Langfuse 中该 Trace 是否有错误标记 |
调整 LB 长连接超时;在客户端实现重连机制 |
| 追踪数据不完整(缺少某些节点) | OpenTelemetry 上下文在异步线程中丢失 | 检查是否使用了自定义线程池而没有传递 Context;查看日志中是否有 Context not found 警告 |
使用 Context.taskWrapping() 包装线程池;确保 Reactor 链路上下文正确传播 |
| Prometheus 指标端点返回 404 | spring-boot-starter-actuator 配置错误或路径被拦截 |
访问 /actuator/prometheus 确认;检查 management.endpoints.web.exposure.include 是否包含 prometheus |
在 application.yml 中添加 management.endpoints.web.exposure.include: health,info,prometheus,metrics |
并发量稍高(>20)就报错 ConnectionPoolTimeout |
HTTP 连接池耗尽,DashScope SDK 默认连接数不够 | 检查 httpclient5 的 maxTotal 和 defaultMaxPerRoute 配置 |
增大连接池:spring.ai.dashscope.client.max-total-connections=200 |
九、最终建议与注意事项
9.1 本项目的五大优势
- 全流程闭环:从图定义、执行、监控到可视化,不是"半成品 demo",而是可落地的工程实践。
- 架构前瞻性 :Spring AI + Langfuse + OTel 的组合是 AI 原生应用可观测性的事实标准,未来迁移成本低。
- 模块化设计:每个节点都是独立的 Bean,新增一个节点只需要写类 + 注册到 Graph,不影响现有逻辑。
- 开发者体验:详细的日志、清晰的错误码、开箱即用的 HTTP 测试文件,让新成员 10 分钟上手。
- 生产基因:K8s 配置、健康检查、Prometheus 指标、资源限制,都是按生产标准配置的。
9.2 生产环境必须注意的事项
-
API 密钥安全
- DashScope Key 使用 K8s Sealed Secrets 或云厂商 KMS 加密
- Langfuse 凭证当前用 Base64,建议升级到 OAuth2.0(Langfuse 企业版支持)
-
网络安全
- 确保应用能访问
dashscope.aliyuncs.com(443 端口)和cloud.langfuse.com(443 端口) - 如果部署在私有云,需要配置出站防火墙规则或代理
- 确保应用能访问
-
性能监控
- 建议每天看一次 Langfuse 的 Dashboard,关注延迟趋势和 Token 消耗趋势
- 当 p95 延迟超过 1s 时,触发自动扩容(K8s HPA)
-
数据持久化
- Langfuse 云端数据自动备份,但建议定期导出关键 Trace 到对象存储(OSS/S3)做长期归档
- 如果本地 SQLite 存储,务必配置定时备份到远程存储,避免容器重启数据丢失
写在最后 :可观测性不是"锦上添花",而是 AI 应用的生命线。当用户投诉"AI 回答得不对"时,你能在 30 秒内定位到是哪个节点的提示词出了问题,这才是这套系统的真正价值。