构建你自己的 Jaeger AI Sidecar:基于 ACP 协议接入任意 LLM 的完整实战指南
2026/9/13 4:52:31 网站建设 项目流程

构建你自己的 Jaeger AI Sidecar:基于 ACP 协议接入任意 LLM 的完整实战指南

【免费下载链接】jaegerCNCF Jaeger, a Distributed Tracing Platform项目地址: https://gitcode.com/GitHub_Trending/ja/jaeger

Jaeger Query 内置了 AI 网关组件,它通过 ACP(Agent Client Protocol) 为骨架,结合 Python 参考实现与 Go 网关源码,完整讲解两条自建路线:Path A(fork 现成的 Gemini 参考实现、替换成你想要的模型)Path B(在任意语言里从零手写 sidecar)。读完你将掌握 sidecar 的全部契约细节:三条必须对齐的常量、八个实现步骤、MCP 工具与 UI 上下文工具(contextual tools)的双路路由,以及一套可直接复用的端到端验证方案。

背景:Jaeger AI 网关与 Sidecar 如何分工

从架构上看,整条链路分属两个进程(详见 网关 README):

  • Jaeger 进程内:AI 网关(ChatHandler)注册在POST /api/ai/chat,接收浏览器的 AG-UIRunAgentInput请求,内部用acp.Connection对 sidecar 发起InitializeNewSessionPrompt三步握手,并把 sidecar 回传的session/update通知翻译成 AG-UI SSE 事件流回浏览器;同时进程内还运行着 Jaeger 的 MCP Server(默认:16687/mcp),向 sidecar 暴露内置的链路查询工具。
  • Sidecar 进程:一个独立的 WebSocket 服务(参考实现默认监听ws://localhost:16688),内部持有 LLM 客户端与 MCP 客户端。它负责把 MCP 工具与网关下发的上下文工具合并交给 LLM,驱动「模型 → 工具 → 模型」的 agentic 循环,并把工具执行进度流式上报给网关。

网关只负责协议翻译与工具派发,模型选择完全由 sidecar 决定。这就是为什么你可以随意更换 LLM 供应商而不动 Jaeger 本体。

在动手之前,先明确两条路线怎么选:

你的处境该走哪条路
想用 OpenAI / Anthropic / Ollama 或你自己的模型Path A:换掉 LLM
想用 Go / Rust / Node 或其它语言写 sidecarPath B:从零构建

无论选哪条,最后的 验证环节 都是一样的。

Path A:换掉 LLM(fork 参考实现,四步走)

仓库在 scripts/ai-sidecar/gemini 提供了一份可运行的 Gemini Python 参考实现。fork 它之后只需替换四样东西,其余部分——WebSocket 服务、ACP 处理器、_meta解析、MCP 桥、上下文工具派发——全部开箱即用

Step 1 — 复制并改名

cp -r scripts/ai-sidecar/gemini scripts/ai-sidecar/myprovider cd scripts/ai-sidecar/myprovider

然后更新pyproject.toml:把google-genai换成你供应商的 SDK,重命名包名;如果想让服务在 Jaeger UI 里以别的名字出现,再改 tracing.py 里的服务名(参考实现默认是jaeger-gemini-sidecar)。

Step 2 — 替换 LLM 客户端

在 sidecar.py 中,Gemini 客户端在 agent 构造时只创建一次:

self._gemini = genai.Client(api_key=config.gemini_api_key)

把它换成你供应商的客户端,然后同步修改 sidecar_config.py 中的配置字段,让环境变量匹配你的供应商(例如用OPENAI_API_KEY替换GEMINI_API_KEY)。参考实现的SidecarConfig是一个 frozen dataclass,validate()会在启动时校验 API key、MCP URL 与 OTLP 端点是否齐全。

Step 3 — 替换 agentic 循环(核心)

sidecar.py 的_run_agentic_gemini_loop实现了整个「模型 → 工具 → 模型」循环,原文档给出了它的骨架:

# 构建工具列表——MCP 工具 + 网关在 session/new 时挂上的上下文工具(已带 ui_ 前缀): mcp_tools = await self._mcp.get_gemini_tools() contextual_tools = self._contextual_tools.get(session_id, []) contextual_tool_names = {t["name"] for t in contextual_tools if t.get("name")} tools_for_llm = merge(mcp_tools, _build_gemini_contextual_tool(contextual_tools)) # 开启聊天并发送用户消息: chat = self._gemini.chats.create(model=..., tools=tools_for_llm, ...) response = await asyncio.to_thread(chat.send_message, user_text) # 循环直到模型不再调用工具: while response.function_calls: function_responses = [] for fc in response.function_calls: if fc.name in contextual_tool_names: # 通过 ACP 扩展方法路由回网关 result = await self._execute_contextual_tool(...) else: # 路由到 Jaeger MCP 服务器 result = await self._execute_tool(...) function_responses.append(...) response = await asyncio.to_thread(chat.send_message, function_responses) return response.text or ""

替换函数体为你的供应商等价实现时,必须保留唯一的路由决策:若模型选中的工具名在contextual_tool_names集合里,就走_execute_contextual_tool(向网关发送 ACP 扩展方法);否则走_execute_tool(调用 Jaeger MCP 服务器)。源码中还配置了AutomaticFunctionCallingConfig(disable=True)关闭 SDK 的自动工具调用,由自己接管循环——这是为了实现上文的双路路由,换成其它供应商时同样建议关闭自动执行。

陷阱提醒:不要重新格式化工具名。上下文工具快照里的名字已经带ui_前缀,请原样传给 LLM,并严格按模型返回的字符串路由;前缀由网关在接收侧剥除。

Step 4 — 翻译工具 schema

每家 LLM 供应商的函数声明格式不同。Gemini 的格式封装在 sidecar_helpers.py:

  • _build_gemini_contextual_tool—— 把 JSON 快照转成 Gemini 的types.Tool(内部把每个工具转成FunctionDeclaration,并直接复用快照里携带的 JSON Schema 作为parameters_json_schema);
  • _extract_function_declaration—— 从 ADK 工具对象中提取单个 Gemini 格式的声明(优先公共 API,退化到 ADK 私有方法作为兜底);
  • JaegerMCPBridge.get_gemini_tools(位于 mcp_bridge.py)—— 把 MCP 工具元数据转成 Geminitypes.Tool列表。

在自建实现里,用你供应商的格式重写这两个转换即可,例如 OpenAI 的tools=[{type: "function", function: {...}}]或 Anthropic 的tools=[{name, description, input_schema}]。上下文快照里是标准 JSON Schema,所以转换通常只是一层薄包装。

Path B:从零构建(八个步骤)

用其它语言从零写一个 sidecar 大约八步。每一步都标注了 Gemini 参考实现里对应的文件,即使不能照抄代码,也能照搬行为。

1. 起一个 WebSocket 服务

监听一个与运维配置的extensions.jaeger_query.ai.agent_url匹配的主机/端口(例如ws://localhost:16688)。每个接入的连接处理一个 ACP 会话,提示词(prompt)处理完毕即关闭连接。参考实现见 gemini/main.py(用websockets.serve绑定端口,并为每个连接创建全新的JaegerSidecarAgent实例以支持并发)与 gemini/sidecar.py 的 handle_websocket。

实现细节上值得注意:handle_websocketsocket.socketpair()把 WebSocket 桥接到 ACP 的 stdio 风格流,从而复用 ACP 库的帧协议实现,避免在本进程里重新实现一套 ACP framing——你在自建时可以借鉴这种「复用官方 SDK 传输层」的思路,或者直接实现 JSON-RPC 帧(见第 2 步)。

2. 在 socket 上讲 ACP JSON-RPC

如果你的语言已有现成 ACP SDK 就直接用;否则自己实现 JSON-RPC 帧——一个 WebSocket 文本帧对应一条 JSON 消息即可,网关侧就是这么做的,参见 ws_adapter.go(它把 gorilla WebSocket 适配成 ACP 运行时需要的io.ReadWriteCloser)。

必须处理三个入站方法:

  • initialize—— 返回你的协议版本与能力声明。网关不声明 fs/terminal 能力,所以别依赖它们。参考实现还校验协议版本与PROTOCOL_VERSION一致,并在agent_capabilities中声明session.close能力;
  • session/new—— 分配一个会话 id 并返回。_meta快照就是在这里到达的,见第 4 步
  • session/prompt—— 跑一轮对话,见第 5~8 步。

参考实现对应initialize/new_session/prompt三个方法(都在 gemini/sidecar.py)。

陷阱提醒:权限请求会被拒绝。网关永远拒绝session/request_permission(网关在Initialize里不声明 fs/terminal 能力,Dispatcher 对session/request_permission恒返回拒绝),所以别浪费时间请求权限。

3. 发现并调用 Jaeger MCP 工具

sidecar 通过 HTTP直连Jaeger 的 MCP 服务器(默认http://127.0.0.1:16687/mcp)。用任意 MCP 客户端库,每个会话调用一次tools/list发现工具,当 LLM 选中某个名字时调用tools/call执行。这些调用不经过网关

参考实现 mcp_bridge.py 的JaegerMCPBridge用 ADK 的MCPToolset+StreamableHTTPConnectionParams连接 MCP,工具发现结果缓存在_tools_by_name字典里(带mcp_discovery_timeout_sec超时,默认 15 秒,可通过JAEGER_MCP_DISCOVERY_TIMEOUT_SEC环境变量调整),call_tool按名字查出工具并run_async执行。

4. 从_meta解析上下文工具快照(最重要的一步)

这是 ACP 规范之外没有任何文档告诉你要做的事。每次session/new,检查请求上的_meta字段:如果包含键jaegertracing.io/contextual-tools,其值就是网关想注册的按轮次(per-turn)UI 工具列表:

{ "_meta": { "jaegertracing.io/contextual-tools": { "tools": [ { "name": "ui_show_flamegraph", "description": "Open the flamegraph view for a given trace_id.", "parameters": { "type": "object", "properties": { ... } } } ] } } }

把这个列表以刚分配的 session id 为键存起来session/prompt时还要用到它,prompt 结束时必须丢弃。

参考实现里,_extract_contextual_tools(sidecar_helpers.py)负责容错解析(_meta缺失、键缺失或载荷畸形都安全返回空列表);new_session(sidecar.py)把解析结果存入self._contextual_tools[session_id]。注意一个 Python ACP 运行时的细节:它会把_meta的内部键摊平进 handler 的**kwargs,所以代码里直接查kwargs字典。

陷阱提醒:名字已经带前缀。每个上下文工具名都以ui_开头,这是刻意设计的:防止 UI 工具遮蔽同名内置 MCP 工具(例如search_traces)。把带前缀的名字原样传给 LLM,返回时由网关剥掉前缀。

5. 合并 MCP 与上下文工具,交给 LLM

session/prompt到达时,把第 3 步的 MCP 工具与第 4 步的上下文工具合并,转换成你的 LLM 期望的格式。参考实现的合并发生在_run_agentic_gemini_loopmcp_tools_build_gemini_contextual_tool(...)的结果一起 append 进tools_for_gemini列表。

同时维护一个上下文工具名的set——第 7 步要靠它判断每个 function call 该往哪里路由。

6. 通过session/update流式上报进度

在 LLM 思考与调用工具期间,向网关发出 ACPsession/update通知。网关会把它们转成 AG-UI SSE 事件转发给浏览器:

你的session/update浏览器看到的内容
AgentMessageChunk(text)TEXT_MESSAGE_CONTENT
start_tool_call(...)TOOL_CALL_START(+ARGS
update_tool_call(...)TOOL_CALL_ARGS/RESULT/END

每个工具调用——无论 MCP 还是上下文——都要用start_tool_call+update_tool_call包裹,UI 才能一致地渲染进度。参考实现的两个执行路径_execute_tool_execute_contextual_tool(sidecar.py)都是如此:先发start_tool_call(status="in_progress"),执行完毕后发update_tool_call(status="completed", ...)

一个值得注意的细节:上下文工具路径故意不填raw_output/content。因为一旦填入,streaming client 就会发出TOOL_CALL_RESULT,从而误导 assistant-ui 以为服务端已经产出结果、跳过浏览器的本地execute()执行。

7. 路由 function call——MCP 还是上下文

当 LLM 产生一个 function call 时,检查名字:

  • 在 MCP 集合里→ 调用 Jaeger MCP 服务器(第 3 步);
  • 在上下文集合里→ 向网关发送 ACP 扩展方法。

扩展方法是第二个关键协议件。方法名为_meta/jaegertracing.io/tools/call带前导下划线),载荷如下:

{ "sessionId": "<the session id from session/new>", "name": "ui_show_flamegraph", "args": { "trace_id": "abc123" } }

网关会立即返回:

{ "result": { "acknowledged": true }, "isError": false }

就这么简单——不会有来自浏览器的真实结果。UI 工具是命令(导航、渲染、过滤)而非查询,所以把这个 ack 当作函数结果喂回给 LLM、继续循环即可;浏览器已经看到你的session/update,正在本地执行副作用。完整的设计论证见 RFC 0002 §6.6 为什么采用 fire-and-forget:UI 工具没有有意义的返回值、同步往返需要额外的回传端点与逐调用 rendezvous 状态、且 ack 方案能让 agentic 循环在同一轮Prompt里继续直到产出最终答案。

参考实现的对应物是_execute_contextual_tool(sidecar.py),核心调用是conn.ext_method(EXT_METHOD_JAEGER_TOOL_CALL, {"sessionId": ..., "name": ..., "args": ...})(约第 253 行)。网关侧handleJaegerToolCall的行为在 dispatcher.go:剥掉ui_前缀 → 用剥离后的名字对ContextualToolsStore中该 session 的快照做校验(未注册则拒绝并返回InvalidParams)→ 记录日志 → 立即返回 ack。

陷阱提醒:前导下划线的怪癖。部分 ACP 库(例如 Python 的)会在发送时自动为扩展方法名补上前导_,所以代码里的常量写作meta/jaegertracing.io/tools/call;另一些库则要求你自己带上。务必确认你的库的行为——线上传输的字节必须是_meta/jaegertracing.io/tools/call。参考实现在 sidecar.py 的注释里专门说明了这一点:与 Go 侧共享的常量是_meta/jaegertracing.io/tools/call,Python 侧因为自动补_而写作去掉前导下划线的形式。

陷阱提醒:不要等浏览器。如果阻塞在扩展方法响应上等待一个「真实」结果,你会死锁。ack 就是结果

8. prompt 结束时清理

session/prompt返回(无论成功、出错还是客户端断开),丢弃第 4 步存下的快照。网关为每个聊天请求开启一个 ACP 会话且从不复用 session id,所以清理是无条件的——直接pop条目即可。

参考实现在promptfinally块里执行self._contextual_tools.pop(session_id, None)(sidecar.py),并实现了close_session做幂等兜底清理(pop(..., None)对从未注册过上下文工具、或已被finally清理过的会话都安全)。

三个必须对齐的常量

以下是双方必须逐字节一致的线上字符串(Go 网关侧的常量定义可追溯到 RFC 0002 与网关实现):

常量出现位置
CONTEXTUAL_TOOLS_META_KEYjaegertracing.io/contextual-toolsNewSessionRequest._meta里的键
ExtMethodJaegerToolCall_meta/jaegertracing.io/tools/callACP 扩展方法,sidecar → 网关
UIToolPrefixui_网关给每个上下文工具名加的前缀

验证它是否工作(端到端冒烟测试)

下面的验证流程对 Path A 与 Path B 都适用。

Gemini 参考实现捷径:第 1、2 步可以合并为一条命令——先export GEMINI_API_KEY=…再执行make run-ai-gemini。该 launcher(Makefile → run.sh)会先跑 preflight 检查 API key、用uv sync引导 Python 工具链、后台启动带示例配置的 Jaeger 并轮询就绪,然后前台运行 sidecar,Ctrl-C 一并退出。想让自己的 fork 也有这种一键体验,在 sidecar 源码旁放一个preflight.sh和一个run.sh——模板见 scripts/ai-sidecar/gemini/run.sh,共享辅助函数见 scripts/ai-sidecar/_lib.sh——再在 Makefile 里加一个run-ai-<name>目标。

1. 用配置好 sidecar 的 Jaeger 启动

# config.yaml extensions: jaeger_query: ai: agent_url: "ws://localhost:16688"
go run ./cmd/jaeger --config config.yaml

仓库自带的示例配置 cmd/jaeger/config.yaml 已经启用了jaeger_query.ai块(agent_url: ws://localhost:16688并开启mcp: {}在进程内提供 MCP 工具),launcher 用的正是这份配置。需要提醒的是:只有配置了非空的ai.agent_url/api/ai/chat端点才会注册。

2. 启动你的 sidecar

Path A 情形:

cd scripts/ai-sidecar/myprovider export OPENAI_API_KEY=... # 或你供应商的 key uv run python main.py

应当看到:

Jaeger ACP Sidecar listening on ws://localhost:16688

3. 发送一个聊天请求

curl -N -X POST http://localhost:16686/api/ai/chat \ -H 'Content-Type: application/json' \ -d '{ "threadId": "t1", "runId": "r1", "messages": [{"role": "user", "content": "what services are running?"}], "tools": [] }'

应当看到一串 AG-UI SSE 帧:RUN_STARTEDTEXT_MESSAGE_START、一个或多个TEXT_MESSAGE_CONTENT,如果 LLM 决定调用 MCP 工具则可能夹着TOOL_CALL_*帧,最后是TEXT_MESSAGE_ENDRUN_FINISHED

4. 测试上下文工具路径

给请求加上一个上下文工具,并让模型使用它:

curl -N -X POST http://localhost:16686/api/ai/chat \ -H 'Content-Type: application/json' \ -d '{ "threadId": "t1", "runId": "r2", "messages": [{"role": "user", "content": "show the flamegraph for trace abc123"}], "tools": [{ "name": "show_flamegraph", "description": "Open the flamegraph view for a trace_id.", "parameters": {"type":"object","properties":{"trace_id":{"type":"string"}},"required":["trace_id"]} }] }'

应当看到show_flamegraphTOOL_CALL_START/TOOL_CALL_ARGS/TOOL_CALL_END帧(注意:没有ui_前缀——网关在转发给浏览器前已经剥掉了)。

5. 借鉴参考测试

Gemini sidecar 自带两个 pytest 文件,值得在你的测试框架里镜像:

  • gemini/test_sidecar_workflow.py —— 通过 WebSocket 连接运行中的 sidecar,用 mocked LLM(FakeAgent)驱动完整的initializesession/newsession/prompt流程,校验流式 ACP 更新与回合结束标记;
  • gemini/test_tracing.py —— 校验 OpenTelemetry 追踪埋点。

6. 确认 UI 自动亮起

你不需要翻转任何 UI 开关。Jaeger 后端会周期性地探测配置的agent_url(默认每 5 秒,可通过jaeger_query.ai.health_check_interval调整),并把结果作为后端能力广播给 UI。sidecar 响应initialize后,在新的浏览器标签页打开 Jaeger UI,聊天界面就会在下一次页面加载时出现;停掉 sidecar,聊天界面以同样方式消失。

深入参考实现:源码细节拾遗

MCP 桥与工具发现

mcp_bridge.py 的JaegerMCPBridge.initialize只做一次工具发现:从StreamableHTTPConnectionParams拉取 ADK 工具列表,逐个提取FunctionDeclaration,组装成单个 Geminitypes.Tool缓存。首次发现失败会直接中断请求(RuntimeError),而不是静默降级——这与「LLM 应把遥测数据当作事实来源,不要凭空臆测」的设计意图一致。

遥测与观测性

参考实现基于 OpenTelemetry 埋点:prompt、agentic 循环、MCP 工具发现/调用、上下文工具派发各有独立 span,Gemini 调用通过opentelemetry-instrumentation-google-generativeai自动插桩,遵循 OTel GenAI 语义约定。工具参数与结果写入 span 属性时经_truncate_for_span截断(上限 65536 字符),避免超大载荷(例如search_traces的大结果集)击穿 OTLP 导出器的属性大小限制。OTLP 默认导出到http://localhost:4317,正好匹配 Jaeger all-in-one 的 OTLP 接收端,因此 sidecar 会作为独立服务出现在 Jaeger UI 中。完整的环境变量与 CLI 参数见 gemini/README.md(--otlp-endpoint/OTEL_EXPORTER_OTLP_ENDPOINT--otlp-insecure/OTEL_EXPORTER_OTLP_INSECURE)。指标目前刻意不导出——Jaeger 不接收 OTLP 指标。

启动器脚本的分工

run.sh 按 preflight →uv sync→ 启动 Jaeger → 前台运行 sidecar 的顺序执行;_lib.sh 提供ai::start_jaeger(用set -m让后台进程自成进程组,退出时按负 PID 整组收割,避免go run包装进程与编译产物脱钩成孤儿进程)、ai::wait_jaeger(轮询http://127.0.0.1:16686/api/v3/services,默认 90 秒超时)与ai::tag(awk 实时给日志行加颜色前缀)。如果你的 fork 要加一键启动,复用这套共享函数即可。

继续深入:到哪里读更多

  • 架构与协议细节:网关 README,包含完整的组件说明、请求时序图与ContextualToolsStore生命周期;
  • 为什么这样设计:RFC 0002:AI 网关上下文工具,重点看 §6.6 的 fire-and-forget 论证;
  • 可以直接抄的成品代码:scripts/ai-sidecar/gemini 目录及 其 README。

【免费下载链接】jaegerCNCF Jaeger, a Distributed Tracing Platform项目地址: https://gitcode.com/GitHub_Trending/ja/jaeger

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询