Trace 层级修复、LangGraph 调用链和外部接入使用说明
agent.return 下面;普通 worker 和外部系统可以通过统一 header / metadata 接入。
修改内容
本轮修改主要解决两个问题:跨 agent 调用时树形结构不符合业务因果关系,以及示例代码同时用 OTel 和 Langfuse 手写同名 span 导致重复节点。
1. 区分 framework tree parent 和 OTel parent worker/context.py · worker/runner.py
trace_parent_span_id 继续用于 OTel / Phoenix 的十六进制 span parent。
新增或使用 framework_parent_span_id 表示 Redis / Dashboard 的框架树 parent。
这样不会把 OTel span id 当成框架 span id,避免 dashboard 树断开或错位。
2. A 调 B 时,B 挂到 A 的工具调用下面 langgraph/tools.py · context.py
LangGraph 的 query_weather 工具节点由 Langfuse LangChain callback 创建。
现在 make_remote_agent_tool() 会从 LangChain 注入的 callbacks.parent_run_id
找到当前 tool observation,并通过 metadata.langfuse_parent_observation_id
显式传给 context.call_agent()。
3. B 返回 A 时,A resume 挂到 agent.return 下面 worker.py · trace-langfuse/langfuse.py
Worker 自动返回时会创建 agent.return observation,并把 ResumeCommand 的
langfuse_parent_observation_id 改为该 return observation。A resume 的
worker.execute 只有在 framework_parent_span_id 以
:agent.return 结尾时,才会挂到 return 节点下,避免普通 resume 被误挂。
4. Trace demo 改成单一 Langfuse 树 samples trace-demo-worker/main.py
示例 worker 删除了手写 OTel span,避免 LangfuseSpanProcessor 再同步出一棵同名
external_complex_pipeline。现在示例只在框架 agent observation 下创建一棵手写
Langfuse 子树。
正确树结构
对于 client -> A -> call_agent(B) -> B -> return(A resume),期望在 Langfuse 上看到:
agent.workflow:A 是跨 suspend/resume 的逻辑容器;
worker.execute 才是一次实际 worker 执行。A resume 挂到
agent.return 下,表达的是“B 返回触发了 A 的下一段执行”。
使用方式
大多数 by-framework 用户不需要直接操作 trace id。正常使用 client、worker、LangGraph 工具即可。 只有外部系统或手动 Langfuse SDK 写入时,才需要显式处理 parent。
# 普通 Worker:无需手动创建根 trace/span
class MyWorker(GatewayWorker):
def get_agent_types(self):
return ["my-agent"]
async def process_command(self, command, context):
await context.emit_chunk("started")
# 框架自动提供:
# client.dispatch -> worker.execute -> my-agent
return {"status": "COMPLETED", "answer": "ok"}
# LangGraph remote tool:使用 make_remote_agent_tool
from by_framework_langgraph import make_remote_agent_tool
weather_tool = make_remote_agent_tool(
context,
tool_name="query_weather",
target_agent_type="weather-agent",
description="查询天气并返回结果",
)
# 工具内部会自动把 agent.call_agent 挂到 query_weather 下。
# 不要自己再手写 langfuse_parent_observation_id。
# 外部系统收到 AskAgentCommand 后,需要保留这些字段
header.trace_id
header.trace_parent_span_id
header.langfuse_parent_observation_id
header.metadata["framework_parent_span_id"]
# 如果外部系统直接写 Langfuse SDK:
external = langfuse.start_observation(
name="external_pipeline",
trace_context={
"trace_id": normalized_trace_id,
"parent_span_id": header.langfuse_parent_observation_id,
},
as_type="span",
)
# trace-demo-worker 现在只保留一棵 Langfuse observation 树
framework_obs = getattr(context, "_langfuse_observation", None)
external = framework_obs.start_observation(
name="external_complex_pipeline",
as_type="span",
input=content,
)
retrieval = external.start_observation(name="Knowledge_Retrieval", as_type="retriever")
llm = external.start_observation(name="LLM_Reasoning_Engine", as_type="generation")
formatter = external.start_observation(name="Result_Formatter", as_type="span")
- 默认创建
client.dispatch。 - 向 command header 注入
trace_id、trace_parent_span_id、Langfuse parent。 - Langfuse 未配置时 best-effort 跳过,不影响发送消息。
- 自动创建
worker.execute和 agent task。 context.call_agent()会创建agent.call_agent。- 任务返回会创建
agent.return并触发 resume parent 传递。
Trace Demo 说明
by-framework-samples/python/workers/trace-demo-worker/main.py
已经调整为单一 Langfuse observation 示例。新的树应当只有一棵
external_complex_pipeline。
排查清单
agent.call_agent 没挂到 query_weather 下 LangGraph
优先检查是否使用 make_remote_agent_tool()。如果自己写 tool,需要从 LangChain
callback manager 中拿当前 tool observation id,并传给 context.call_agent(metadata=...)。
A resume 没挂到 agent.return 下 ResumeCommand
检查 ResumeCommand 是否包含 metadata.framework_parent_span_id=...:agent.return
以及 langfuse_parent_observation_id。只有这两个条件满足,resume 的
worker.execute 才会挂到 return observation 下。
external_complex_pipeline 出现两次 Demo / external integration
说明同一段业务同时用了手写 Langfuse observation 和 OTel span。作为展示文档,建议二选一: 要么只用 Langfuse SDK,要么只用 OTel,不要同名重复写入。
FAQ
- 为什么要同时有
trace_parent_span_id和framework_parent_span_id? -
因为前者服务 OTel / Phoenix,是 16 位十六进制 span id;后者服务框架自己的 Redis/Dashboard 树,
通常是
message_id:operation这种稳定字符串。混用会导致层级错位。 - 为什么
agent.return显示耗时 0.00s? -
它表示“B 将 ResumeCommand 入队给 A”的动作,不代表 B 的业务耗时。B 的耗时在 B 的
worker.execute/ LangGraph / LLM 节点上。 - 外部系统必须接 by-framework SDK 吗?
- 不必须。外部系统只要遵守 header 字段和 Langfuse parent observation 传递规则,就可以挂到同一 trace。 Python Read SDK / Dashboard 后续可以从 Redis、Langfuse、Phoenix 多源读取。