by-framework trace update · 2026-06-10

Trace 层级修复、LangGraph 调用链和外部接入使用说明

结论:这次修改把 trace 链路从“同 trace id 下并列展示”升级为“按真实因果关系挂载”。 A 调 B 时,B 会挂到 A 的工具调用节点下面;B 返回 A 时,A resume 会挂到 agent.return 下面;普通 worker 和外部系统可以通过统一 header / metadata 接入。
3
核心 parent 通道:OTel、Langfuse、Redis tree
2
关键因果边:call_agent 与 agent.return
1
Trace demo 改为单一 Langfuse observation 树
0
写入失败不影响任务主流程

修改内容

本轮修改主要解决两个问题:跨 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 上看到:

client.dispatch:A └─ client.dispatch:A └─ agent.workflow:A └─ worker.execute (A initial) └─ A └─ A:langgraph └─ tools └─ query_weather └─ agent.call_agent:B └─ agent.workflow:B └─ worker.execute (B) └─ B └─ B:langgraph └─ agent.return └─ worker.execute (A resume) └─ A └─ A:langgraph
!
不要把 agent.workflow 和具体执行段混为一谈。 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 侧
  • 默认创建 client.dispatch
  • 向 command header 注入 trace_idtrace_parent_span_id、Langfuse parent。
  • Langfuse 未配置时 best-effort 跳过,不影响发送消息。
Worker 侧
  • 自动创建 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

worker.execute └─ trace-demo-agent └─ external_complex_pipeline ├─ Knowledge_Retrieval │ ├─ VectorDB_Search │ └─ Rerank_Candidates ├─ LLM_Reasoning_Engine └─ Result_Formatter

排查清单

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_idframework_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 多源读取。