Langfuse In-App Agent 架构深度解析:单一事件日志与三种派生视图的工程契约
2026/9/10 1:03:36 网站建设 项目流程

Langfuse In-App Agent 架构深度解析:单一事件日志与三种派生视图的工程契约

【免费下载链接】langfuse🪢 Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. 🍊YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse

Langfuse 的项目级内嵌 AI 助手(In-App Agent)采用"浏览器观察、Worker 持久化执行"的架构:用户在产品 UI 中与助手对话,真实的一次次运行(run)在后台 Worker 中持续执行,浏览器只负责观察其持久化的事件流。本文以web/src/features/in-app-agent/ARCHITECTURE.md为骨架,结合packages/sharedwebworker三端源码,系统讲解"一条日志、三种派生"(one log, three derivations)的表示契约、fold once渲染不变量、代码职责边界、持久化执行链路以及改动的红线规则。读完你不仅能理解抽屉式聊天界面的构建动机,还能掌握让浏览器、持久化转录与运行生命周期保持一致的工程方法论。

为什么抽屉这样构建:单一事实来源的三种派生

In-App Agent 的架构核心可以浓缩为一句话:对话的每一种消息表示,都从唯一一个地方派生而来——in_app_agent_events表,并按照每个会话(conversation)内的sequence_number排序。规范消息(canonical)、展示消息(display)与回放消息(replay)不会被分别持久化成多个独立的转录事实来源。

派生视图生产者消费者形态永不应该出现的地方
Canonical messages(规范消息)createConversationMessageAccumulator(位于 packages/shared/src/in-app-agent/server/persistence.ts)getConversation线(wire)、AG-UI 种子(seed)、反馈身份、标题推断以稳定的 AG-UI 消息 ID 为键的完整 assistant 消息,保留runId,保留进行中的工具调用(in-flight tool calls)在给实时 agent 播种(seed)之前被裁剪(prune)或重排(reorder)
Display state + projection(展示状态与投影)web/src/features/in-app-agent/lib/display.ts 中的record*累积函数与projectInAppAgentMessagesForDisplay折叠仅在InAppAiAgentProvider中渲染一次一个"边车"(sidecar),描述交错出现的推理与工具调用应插在何处,外加合成的display-text-<id>-N分段写回服务器,或喂给 agent
Replay messages(回放消息)getConversationMessagesForReplay(位于 packages/shared/src/in-app-agent/server/persistence.ts)恢复运行(resuming run)时的模型上下文剥离了 reasoning、redirect 结果、runId以及未配对工具调用的消息被渲染,或用作客户端水合(hydration)快照

三种派生的存在是因为它们回答的问题完全不同:发生了什么(what happened)、应该长什么样(how it should look)、模型下一步应该看到什么(what the model should see next)。把其中任意两个混淆,正是这个功能反复踩坑的失败模式,所以上面这张表不是描述,而是契约(the contract)。

从源码可以印证这一点:getConversationMessagesForReplay在 persistence.ts 中先取规范消息,再调用sanitizeConversationMessagesForReplay进行净化——过滤掉reasoning角色消息、丢弃 redirect action 的工具结果、剥离未配对的 assistant 工具调用、删除空 assistant 消息、并去除 assistant 消息上的runId(见 persistence.ts)。同一份事件日志,通过不同的折叠与净化管线,服务三类完全不同的消费方。

核心不变量:fold once(只折叠一次)

展示投影(display projection)恰好执行一次,且只在浏览器渲染时执行。这不是风格偏好,而是正确性要求。

投影为何是"有损"的

投影在一个特定意义上是有损的:它会在 assistant 消息的第一个交错块(interleaved block)处截断该消息,并把续文移动到一个合成的兄弟消息中。这对渲染是正确的,但对其他任何用途都是错误的。

文档记录了一个真实踩过的坑:该功能最初上线时,服务器也做了投影,于是浏览器在启动实时 AG-UI agent 时,用被截断的消息作为种子。AG-UI 按消息 ID 追加TEXT_MESSAGE_CONTENT,所以恢复运行的下一段增量落到了被截断的种子上,续文从规范转录中消失,直到运行结束并重新水合才恢复。

当时的修复不是换一个访问器,而是停止折叠两次。现在的线(wire)上携带的是"规范消息 + 作为边车的展示状态",唯一的一次折叠发生在实时路径原本就会折叠的地方——即浏览器渲染时。

两个必须明确写出来的推论

  • 裁剪(pruning)本身也是一种展示行为。dropUnpairedAssistantToolCallsdropEmptyAssistantMessages(均位于 packages/shared/src/in-app-agent/messages.ts)只在渲染时、且只针对已settled(结束)的转录执行。实时种子必须保留进行中的工具调用,否则后续到达的TOOL_CALL_RESULT没有可挂靠的对象,AG-UI 会追加一条孤儿的tool消息,被抽屉静默丢弃。
  • 边车是派生数据,绝不权威。即使边车丢失,转录依然正确,只是变得更"扁平"。因此deserializeInAppAgentDisplayState(lib/display.ts)在反序列化失败时回退为空状态而不是抛异常。

在 display.ts 的projectInAppAgentMessagesForDisplay中可以看到投影的具体形态:它遍历规范消息中的 assistant 工具调用,依据展示状态中的toolCallPlacements生成display-tool-<id>合成消息,再依据textByMessageId中的分段生成display-text-<id>-N文本段——这些全部是边车状态,规范消息本身始终不动。

代码在哪里:shared / web / worker 的职责边界

代码布局遵循一条清晰原则:packages/shared只放 web 与 worker 都需要的东西;仅 web 需要的逻辑即使运行在服务器上,也留在 web。

  • worker 运行 agent,绝不渲染。因此 Mastra 适配、工具、埋点、续跑处理、prompt 加载、技能(skills)与沙箱提供者都放在worker/src/features/in-app-agent/runtime/
  • shared 只拥有跨进程的持久化契约与存储行为:事件日志、规范消息累积、回放、生命周期、审批事件、MCP 策略、工具结果脱敏(tool-result redaction)以及种子 prompt。shared 的持久化层对渲染一无所知
  • web 拥有:watch 帧(watch framing)、展示记录(display recording)、投影、ID、反馈/来源 schema,以及会话访问。

两个消息裁剪辅助函数是有意为之的例外:dropUnpairedAssistantToolCallsdropEmptyAssistantMessages裁剪的是AgUiMessage形状而不是描述展示样式,且回放需要从 worker 侧调用它们,所以它们放在 packages/shared/src/in-app-agent/messages.ts 中,被回放净化与渲染时沉降(settling)两处共用。

持久化执行路径:一条贯穿浏览器、服务器与 Worker 的链路

ARCHITECTURE.md 用一张 mermaid 流程图勾勒出完整链路:

链路中的关键语义:

  1. 浏览器侧的BackgroundExecutionSessionController通过 tRPC 发起start并获取快照;
  2. web 服务器(router.ts/backgroundRunService.ts)入队(enqueue)到 Worker;
  3. Worker 中的executeInAppAgentRun驱动runtime/agent + tools + sandbox执行,并写入in_app_agent_events事件日志;
  4. 浏览器通过 SSE 从持久化的游标(cursor)之上续接 tail(watch/route.ts);
  5. 浏览器拿到"规范消息 + displayState"边车后,投影并平滑渲染到抽屉。

关闭抽屉只是分离观察,并不会取消运行(closing the drawer detaches observation without cancelling the run)。重新打开时,先水合一个快照,再从持久化的游标之上恢复 tail。会话(session)是浏览器中规范转录、审批、附件、取消与当前运行状态的唯一所有者(the sole owner)。

变更规则:给未来的改动划红线

ARCHITECTURE.md 给出了四条不可逾越的改动纪律:

  • 新增一种表示 = 在上面的表格中新增一行,并写清它的生产者、消费者、以及它"永不应该出现的地方"。如果填不满这几项,说明你很可能只是在伸手够一个已经存在的表示。
  • 在通往实时 agent 的路上,不要投影、裁剪或重排消息。种子必须保持完整。
  • 让快照与 tail 停留在同一个游标上(Keep the snapshot and the tail on one cursor)。
  • 在改动任何涉及排序、压缩(compaction)或持久化的代码之前,先读README.md中关于 AG-UI 事件语义的部分。

与 README 的配合阅读:操作视角与实现视角的互补

web/src/features/in-app-agent/ARCHITECTURE.md与其同目录的 README.md 分工明确:

  • README 是操作指南:每个文件拥有什么、一次运行如何流转、沙箱与 MCP 授权如何工作;
  • ARCHITECTURE 解释无法从任何单个文件推断出的部分:为什么表示层长这样、折叠不变量从何而来、代码为什么分布在这三个包中。

例如 README 补充了运行生命周期(提交一条用户消息 → 会话安装持久化规范消息 + 展示状态 + 游标 → 快照独占消息/审批/运行/取消/附件 → 关闭抽屉调用detach()只停止观察)、客户端状态归属(React Query 拥有列表与持久化会话查询;BackgroundExecutionSessionController拥有实时执行事实;展示节奏保持在useSmoothStreamingMessages,规范 AG-UI 消息绝不为动画而重写),以及一套固定的生命周期策略常量(队列超时 300000 ms、最大运行时长 900000 ms、审批 TTL 86400000 ms,均为 web 与 worker 共享的常量,确保两端不会发散)。

总结

Langfuse In-App Agent 的架构设计可以浓缩为三个相互支撑的决策:

  1. 单一事件日志in_app_agent_events+ 会话内sequence_number)作为唯一事实来源,canonical / display / replay 三种视图全部由它派生,互不独立持久化;
  2. 只折叠一次:有损的展示投影只在浏览器渲染时执行一次,线(wire)上传输的是"规范消息 + 边车展示状态",从根上杜绝服务器与浏览器双重折叠导致的转录丢失;
  3. 清晰的跨进程职责边界:shared 只持有持久化契约与存储行为,worker 执行永不渲染,web 拥有渲染、投影与观察;让"发生了什么、该长什么样、模型该看什么"三个问题各有各的答案。

这套契约不仅保证了抽屉界面与后台持久化运行之间的连贯性,也为后续任何新增表示或改动提供了可校验的边界——如果新增的表示无法填满"生产者 / 消费者 / 永不出现的位置"三栏,它大概率就是多余的。

【免费下载链接】langfuse🪢 Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. 🍊YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse

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

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

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

立即咨询