拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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

构建你自己的 Jaeger AI Sidecar基于 ACP 协议接入任意 LLM 的完整实战指南【免费下载链接】jaegerCNCF Jaeger, a Distributed Tracing Platform项目地址: https://gitcode.com/GitHub_Trending/ja/jaegerJaeger Query 内置了 AI 网关组件它通过 ACPAgent Client Protocol 为骨架结合 Python 参考实现与 Go 网关源码完整讲解两条自建路线Path Afork 现成的 Gemini 参考实现、替换成你想要的模型与Path B在任意语言里从零手写 sidecar。读完你将掌握 sidecar 的全部契约细节三条必须对齐的常量、八个实现步骤、MCP 工具与 UI 上下文工具contextual tools的双路路由以及一套可直接复用的端到端验证方案。背景Jaeger AI 网关与 Sidecar 如何分工从架构上看整条链路分属两个进程详见 网关 READMEJaeger 进程内AI 网关ChatHandler注册在POST /api/ai/chat接收浏览器的 AG-UIRunAgentInput请求内部用acp.Connection对 sidecar 发起Initialize→NewSession→Prompt三步握手并把 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换掉 LLMfork 参考实现四步走仓库在 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_keyconfig.gemini_api_key)把它换成你供应商的客户端然后同步修改 sidecar_config.py 中的配置字段让环境变量匹配你的供应商例如用OPENAI_API_KEY替换GEMINI_API_KEY。参考实现的SidecarConfig是一个 frozen dataclassvalidate()会在启动时校验 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..., toolstools_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(disableTrue)关闭 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_websocket用socket.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—— 跑一轮对话见第 58 步。参考实现对应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 的MCPToolsetStreamableHTTPConnectionParams连接 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-turnUI 工具列表{ _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_toolssidecar_helpers.py负责容错解析_meta缺失、键缺失或载荷畸形都安全返回空列表new_sessionsidecar.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_CONTENTstart_tool_call(...)TOOL_CALL_STARTARGSupdate_tool_call(...)TOOL_CALL_ARGS/RESULT/END每个工具调用——无论 MCP 还是上下文——都要用start_tool_callupdate_tool_call包裹UI 才能一致地渲染进度。参考实现的两个执行路径_execute_tool与_execute_contextual_toolsidecar.py都是如此先发start_tool_call(statusin_progress)执行完毕后发update_tool_call(statuscompleted, ...)。一个值得注意的细节上下文工具路径故意不填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-forgetUI 工具没有有意义的返回值、同步往返需要额外的回传端点与逐调用 rendezvous 状态、且 ack 方案能让 agentic 循环在同一轮Prompt里继续直到产出最终答案。参考实现的对应物是_execute_contextual_toolsidecar.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/callPython 侧因为自动补_而写作去掉前导下划线的形式。陷阱提醒不要等浏览器。如果阻塞在扩展方法响应上等待一个「真实」结果你会死锁。ack 就是结果。8. prompt 结束时清理当session/prompt返回无论成功、出错还是客户端断开丢弃第 4 步存下的快照。网关为每个聊天请求开启一个 ACP 会话且从不复用 session id所以清理是无条件的——直接pop条目即可。参考实现在prompt的finally块里执行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。该 launcherMakefile → run.sh会先跑 preflight 检查 API key、用uv sync引导 Python 工具链、后台启动带示例配置的 Jaeger 并轮询就绪然后前台运行 sidecarCtrl-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:16688go 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. 启动你的 sidecarPath A 情形cd scripts/ai-sidecar/myprovider export OPENAI_API_KEY... # 或你供应商的 key uv run python main.py应当看到Jaeger ACP Sidecar listening on ws://localhost:166883. 发送一个聊天请求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_STARTED、TEXT_MESSAGE_START、一个或多个TEXT_MESSAGE_CONTENT如果 LLM 决定调用 MCP 工具则可能夹着TOOL_CALL_*帧最后是TEXT_MESSAGE_END、RUN_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_flamegraph的TOOL_CALL_START/TOOL_CALL_ARGS/TOOL_CALL_END帧注意没有ui_前缀——网关在转发给浏览器前已经剥掉了。5. 借鉴参考测试Gemini sidecar 自带两个 pytest 文件值得在你的测试框架里镜像gemini/test_sidecar_workflow.py —— 通过 WebSocket 连接运行中的 sidecar用 mocked LLMFakeAgent驱动完整的initialize→session/new→session/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 工具发现/调用、上下文工具派发各有独立 spanGemini 调用通过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::tagawk 实时给日志行加颜色前缀。如果你的 fork 要加一键启动复用这套共享函数即可。继续深入到哪里读更多架构与协议细节网关 README包含完整的组件说明、请求时序图与ContextualToolsStore生命周期为什么这样设计RFC 0002AI 网关上下文工具重点看 §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),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门