LangFuse自定义Graph节点调试信息收集

0 阅读4分钟

开端

在使用Langfuse的过程中,官方自带的集成langGraph中,进行收集到的节点和LLM的调用信息,无法进行定制化,只要调用就收集,并且如果Graph嵌套较多,Trace收集到的日志嵌套很深,有用的信息会被循环和嵌套给“淹没”掉,并且成本是很高的,一次运行可以达到1000-1500的次数收集

所以我使用原生的langfuseSDK的方法进行定制,只收集我想要收集的,这样可以控制日志的链路同时也可以控制成本

一句话:一次注入、全链共享;只记录关键、剔除冗馀

使用自定义之后的日志收集,嵌套层级最大只有3层,并且无循环,只有干练的关键节点的信息,并且收集到的日志成本变为20-50左右,降低50倍

1. 设计目标 & 痛点

痛点目标
Trace 层级过深,视图臃肿单 Trace 统御全链
全量回调,成本高仅记录关键 Span + LLM Generation
Tracing 逻辑散落业务装饰器自动化,业务函数零耦合
代码入侵严重Tracing 信息深藏 metadata.tracing,接口干净

2. 总览架构图

在这里插入图片描述

3、关键概念

  • lfnew Langfuse({...}) 实例,入口处一次性创建并塞进 config
  • traceId — 由 Langfuse 自动生成的 UUID,贯穿整条调用链
  • @GraphNode — 节点装饰器,负责
    1. 开 Span
    2. (可选)注入 LLM Generation 回调
    3. 收尾 Span
  • withGen — 装饰器参数
    • true → 记录 LLM Generation
    • false → 仅记录 Span
  • inputKeys - span节点的Input输入

使用这个参数可以控制span节点的输入信息,从state中筛选出来我们需要查询的参数,而不是全部

4. 实现细节

4.1 入口初始化

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 装饰器核心

// 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 节点书写范式


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 子图透传策略

const result = await executeGraph.invoke(
      {
        plannerPartResult: currentItem,
        plannerPartIndex: currentIndex,
        inputParam: inputParam,
        plannerResult: plannerResult,
        userInput: userInput,
        intentResultType: intentResultType,
        envelopeTotalAgent,
        runId,
      },
      config
);

4.5 LLM 调用规范

export async function intentRecognitionModuleV2(userInput: string, config: any) {
  const chain = prompt.pipe(model).pipe(parse);
  let result = await chain.invoke({ userInput: userInput }, config); // 明确指定非流式);
}

5. 总结

刚开始的实现,我是没有先考虑装饰器的,因为我喜欢函数式编程,并不是很喜欢类,并且使用装饰器要创建类,装饰器只能在类的方法上面使用,所以我刚开始考虑的并不是装饰器,而是函数内部调用功能函数就可以

但是发现不够优雅,并且节点函数本来应该只负责业务逻辑的,加过多的系统函数进行,会导致业务逻辑模糊,很大程度上会降低代码的可读性

于是我找到高阶函数的使用,在函数外面包一层函数**,如果我想要的功能过多,其实就需要包多层函数,有点像回调函数的“回调地狱”,所以我没考虑高阶函数**,到此或许函数式编程无法在满足优雅实现功能的需求啦,所以我考虑使用类中的方法来

这样创建类的方法,使用的时候只需要实例化即可,并且可以使用优雅的装饰器