LangFuse自定义Graph节点调试信息收集
开端
在使用Langfuse的过程中,官方自带的集成langGraph中,进行收集到的节点和LLM的调用信息,无法进行定制化,只要调用就收集,并且如果Graph嵌套较多,Trace收集到的日志嵌套很深,有用的信息会被循环和嵌套给“淹没”掉,并且成本是很高的,一次运行可以达到1000-1500的次数收集
所以我使用原生的langfuseSDK的方法进行定制,只收集我想要收集的,这样可以控制日志的链路同时也可以控制成本
一句话:一次注入、全链共享;只记录关键、剔除冗馀
使用自定义之后的日志收集,嵌套层级最大只有3层,并且无循环,只有干练的关键节点的信息,并且收集到的日志成本变为20-50左右,降低50倍
1. 设计目标 & 痛点
| 痛点 | 目标 |
|---|---|
| Trace 层级过深,视图臃肿 | 单 Trace 统御全链 |
| 全量回调,成本高 | 仅记录关键 Span + LLM Generation |
| Tracing 逻辑散落业务 | 装饰器自动化,业务函数零耦合 |
| 代码入侵严重 | Tracing 信息深藏 metadata.tracing,接口干净 |
2. 总览架构图

3、关键概念
- lf —
new Langfuse({...})实例,入口处一次性创建并塞进 config - traceId — 由 Langfuse 自动生成的 UUID,贯穿整条调用链
- @GraphNode — 节点装饰器,负责
- 开 Span
- (可选)注入 LLM Generation 回调
- 收尾 Span
- withGen — 装饰器参数
true→ 记录 LLM Generationfalse→ 仅记录 Span
- inputKeys - span节点的Input输入
使用这个参数可以控制span节点的输入信息,从state中筛选出来我们需要查询的参数,而不是全部
4. 实现细节
4.1 入口初始化
▼javascript复制代码const lf = new Langfuse({ publicKey, secretKey }); const trace = lf.trace({ userId, sessionId, tags: ['v2'] }); await graph.streamEvents(data, { version: 'v2', configurable: { trace, lf }, });
4.2 GraphNode 装饰器核心
▼javascript复制代码// decorators/GraphNode.ts import { CallbackHandler } from 'langfuse-langchain'; // 获取 input,只保留关心的键 function pickInput(state, inputKeys) { if (!inputKeys?.length) return { userInput: state?.userInput }; return inputKeys.reduce((obj, key) => { if (Object.prototype.hasOwnProperty.call(state, key)) obj[key] = state[key]; return obj; }, {}); } // 合并 callbacks,保证为数组 function mergeCallbacks(cfg, span, withGen) { let callbacks = []; if (cfg.callbacks) { callbacks = Array.isArray(cfg.callbacks) ? cfg.callbacks : [cfg.callbacks]; } if (withGen) { callbacks.push(new CallbackHandler({ root: span })); } return callbacks; } //LangFuse日志收集-装饰器 export function GraphNode(name: string, withGen = false, inputKeys: string[] = []) { return function <T extends (...args: any[]) => Promise<any>>(orig: T): T { const wrapped = (async (...args: any[]) => { const [state, cfg = {}] = args; const { configurable = {} } = cfg; const { lf, trace } = configurable; if (!lf || !trace) { throw new Error(`缺少 lf 信息:请确保在 metadata.tracing 中传入 lf 与 traceId`); } // 关心的 input const safeInput = pickInput(state, inputKeys); // 开 span const span = trace.span({ name, input: safeInput }); // runtimeCfg 传递下去 const runtimeCfg = { ...cfg, configurable: { ...configurable, trace: span }, callbacks: mergeCallbacks(cfg, span, withGen), }; try { const result = await orig.apply(null, [state, runtimeCfg]); console.log('开始span结束'); await span.end({ output: result }); return result; } catch (e) { await span.end({ error: String(e) }); throw e; } }) as T; return wrapped; }; } // 方法装饰器,将类方法包装为 GraphNode export function GraphNodeMethod(name: string, withGen = false, inputKeys: string[] = []) { return function (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) { const fn = descriptor.value; descriptor.value = GraphNode(name, withGen, inputKeys)(fn); }; }
4.3 节点书写范式
▼javascript复制代码class SupervisorNodeHandlers { // //1、第一步进行意图识别 @GraphNodeMethod('intentRecognitionNode', true, ['userInput', 'inputParam']) async intentRecognitionNode(state: typeof InputStateAnnotation.State, config: any) { const { userInput, inputParam, lf, trace } = state; let { sessionId, messageId } = inputParam; // 意图识别-函数执行 const result: any = await intentRecognitionModuleV2(userInput, config); let goto = END; return new Command({ goto: goto, update: { intentResult: result, intentResultType, envelopeTotalAgent }, }); } }
业务层只需 透传 **config**,其余由装饰器托管。
4.4 子图透传策略
▼javascript复制代码const result = await executeGraph.invoke( { plannerPartResult: currentItem, plannerPartIndex: currentIndex, inputParam: inputParam, plannerResult: plannerResult, userInput: userInput, intentResultType: intentResultType, envelopeTotalAgent, runId, }, config );
4.5 LLM 调用规范
▼javascript复制代码export async function intentRecognitionModuleV2(userInput: string, config: any) { const chain = prompt.pipe(model).pipe(parse); let result = await chain.invoke({ userInput: userInput }, config); // 明确指定非流式); }
5. 总结
刚开始的实现,我是没有先考虑装饰器的,因为我喜欢函数式编程,并不是很喜欢类,并且使用装饰器要创建类,装饰器只能在类的方法上面使用,所以我刚开始考虑的并不是装饰器,而是函数内部调用功能函数就可以
但是发现不够优雅,并且节点函数本来应该只负责业务逻辑的,加过多的系统函数进行,会导致业务逻辑模糊,很大程度上会降低代码的可读性
于是我找到高阶函数的使用,在函数外面包一层函数**,如果我想要的功能过多,其实就需要包多层函数,有点像回调函数的“回调地狱”,所以我没考虑高阶函数**,到此或许函数式编程无法在满足优雅实现功能的需求啦,所以我考虑使用类中的方法来
这样创建类的方法,使用的时候只需要实例化即可,并且可以使用优雅的装饰器
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
