ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LangGraph与Langfuse构建可观测AI工作流:从编排到模型优化实战

LangGraph与Langfuse构建可观测AI工作流:从编排到模型优化实战 这类工具链组合最值得先看的不是功能列表而是能不能在你自己的开发环境里稳定跑起来以及从单任务到批量任务、从本地调试到生产部署的完整链路是否清晰。LangGraph、Langfuse、量化感知训练QAT和SFTTrainer这几个词放在一起指向的是一个典型的AI应用开发与优化场景用LangGraph编排复杂的工作流用Langfuse进行全链路追踪和评估同时为了在资源受限的环境比如边缘设备部署需要考虑使用QAT对模型进行轻量化而SFTTrainer则用于特定任务的模型微调。如果你正在构建一个需要多步骤决策、状态管理、并能对每一步进行观测和调优的AI应用比如智能客服、复杂文档处理流水线那么这个组合就值得你花时间。但别急着把所有组件一起装上我建议的路径是先确保LangGraph的基础工作流能跑通再接入Langfuse观察最后再根据实际部署需求考虑是否引入QAT和SFTTrainer进行模型层面的优化。下面我会按照一个实际项目从零搭建到考虑优化的顺序拆解每一步该做什么、注意什么以及如何判断每一步是否成功。1. 环境准备与核心组件定位先搞清楚每个工具到底管哪一段在开始写任何代码之前必须明确每个组件在你的技术栈里扮演什么角色以及它们之间的依赖关系。盲目安装只会带来无尽的版本冲突。1.1 LangGraph你的工作流“总指挥”LangGraph不是LangChain的替代品而是它的一个扩展专门解决有状态、多步骤、可能循环或分支的工作流编排问题。你可以把它理解为一个为AI智能体Agent或复杂任务定制的流程图执行引擎。核心价值它帮你把“聊天”、“检索”、“代码执行”、“判断”等节点用图的方式连接起来并管理整个流程的“状态”State。比如一个客服机器人先理解问题再去查知识库如果没查到就转人工这个决策流程用LangGraph建模就很自然。和LangChain的关系LangChain提供了大量的基础组件LLM调用、文档加载器、工具等而LangGraph提供了更强大的流程编排能力。你通常需要同时安装langchain和langgraph。最新版关注点最新版本例如0.0.x系列可能强化了“静态循环”对于固定次数的循环优化、更灵活的State设计以及与LangGraph Studio的可视化调试集成。安装时务必指定版本避免被自动升级到不兼容的版本。1.2 Langfuse你的工作流“黑匣子”与“仪表盘”Langfuse是一个开源的LLM应用观测平台。当你的LangGraph工作流运行时Langfuse可以记录下每一次LLM调用、每一个工具执行、整个工作流的输入输出、耗时、token消耗、甚至自定义的评估分数。核心价值问题复现和性能优化。当用户反馈“机器人回答错了”你可以通过Langfuse快速定位到是哪个环节的LLM调用出了问题输入是什么输出是什么。你也可以用它来对比不同模型或参数的效果。部署方式你可以使用Langfuse Cloud托管服务也可以本地部署其开源版本。对于生产环境本地部署是更常见的选择需要准备PostgreSQL数据库。与LangGraph集成通过langfuse提供的回调处理器Callback Handler或装饰器decorators可以很方便地将LangGraph的执行轨迹发送到Langfuse服务器。你需要关注的是集成是否顺畅数据是否完整。1.3 量化感知训练QAT为了“上船”或“上端”的模型瘦身术QAT是一种模型压缩技术旨在让大模型能在资源有限的设备如手机、Jetson等边缘计算设备上高效运行。它在训练阶段就模拟低精度如INT8计算的影响让模型适应量化从而在真正部署时精度损失最小。核心价值降低模型推理时的内存占用和计算延迟提升吞吐量。对于需要实时响应或离线部署的LangGraph应用至关重要。何时考虑不要一开始就做QAT先确保你的模型无论是直接调用API还是部署本地模型在FP32或FP16精度下工作流逻辑和效果都是正确的。QAT是部署前端的优化步骤。常用工具PyTorch提供了torch.ao.quantization包对于Hugging Face Transformers模型可以结合optimum和intel-extension-for-transformers等库。1.4 SFTTrainerHugging Face的微调“脚手架”SFTTrainerSupervised Fine-Tuning Trainer是Hugging Facetrl库中的一个高级训练器它简化了基于预训练模型进行有监督微调的过程。核心价值提供了数据集格式化、训练循环、支持QLoRA等高效微调技术的一站式解决方案。如果你的LangGraph工作流中需要用一个针对特定任务如特定领域的问答、格式生成微调过的模型SFTTrainer是常用的实现工具。与工作流的关系你可以使用SFTTrainer产出的微调模型作为LangGraph工作流中的一个LLM节点。它属于模型供给层。1.5 环境安装清单与顺序基于以上理解我建议按以下顺序准备环境并严格记录版本号# 1. 创建并激活虚拟环境强烈建议 python -m venv langgraph-demo source langgraph-demo/bin/activate # Linux/macOS # 或 .\langgraph-demo\Scripts\activate # Windows # 2. 安装核心编排与基础框架 # 指定版本安装避免后续兼容性问题 pip install langgraph0.0.40 langchain0.1.0 # 3. 安装观测平台Langfuse的客户端 # 如果你使用Langfuse Cloud只需要这个客户端 pip install langfuse # 如果你要本地部署Langfuse需要另外部署其服务器端项目涉及Docker和数据库。 # 4. 安装模型训练与优化相关库按需 # 用于微调 pip install trl transformers datasets peft accelerate # 用于量化可选部署阶段再深入 pip install optimum # pip install intel-extension-for-transformers # 如果需要Intel优化的QAT # 5. 记录环境 pip freeze requirements.txt关键检查点运行python -c “import langgraph; print(langgraph.__version__)”确认安装成功。确保你的Python版本在3.8以上。2. 第一站用LangGraph构建一个可运行的最小工作流不要一上来就想复杂的业务逻辑。我们先构建一个最简单的、包含两个节点的链式工作流目标是验证环境是否通畅并理解LangGraph的核心概念。2.1 理解State工作流的“记忆体”LangGraph的核心是围绕State对象运转的。State是一个字典或Pydantic模型在所有节点间共享和传递。你需要先定义State里有什么。from typing import TypedDict, Annotated from langgraph.graph.message import add_messages import operator # 1. 定义State。这里我们定义一个最简单的状态包含对话历史和最终答案。 class State(TypedDict): # add_messages是一个LangGraph提供的特殊操作用于高效追加消息 messages: Annotated[list, add_messages] # 一个普通的字符串字段用于存放最终答案 final_answer: str # 后续的每个节点函数都会接收这个State并返回一个更新后的State或部分更新。2.2 创建节点与边组装你的流程图节点是函数边决定了执行顺序。我们创建两个节点一个生成问题一个回答问题。from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, AIMessage import os # 设置你的OpenAI API Key或其他LLM提供商 os.environ[“OPENAI_API_KEY”] “your-api-key-here” # 初始化一个LLM llm ChatOpenAI(model“gpt-3.5-turbo”) # 2. 定义节点函数 def generate_question(state: State) - State: 节点A生成一个问题 # 从历史消息中获取上下文初始为空 history state[“messages”] # 让LLM生成一个问题 prompt “请生成一个关于Python编程的简单问题。” message HumanMessage(contentprompt) # 注意这里我们直接调用LLM实际复杂场景可能用LangChain的Runnable response llm.invoke([message]) generated_question response.content # 更新State将生成的问题添加到消息历史并存入final_answer临时 new_messages state[“messages”] [HumanMessage(contentgenerated_question)] return {“messages”: new_messages, “final_answer”: generated_question} def answer_question(state: State) - State: 节点B回答最新的问题 # 获取最新的消息即上一步生成的问题 history state[“messages”] latest_question history[-1].content if history else “” # 让LLM回答这个问题 prompt f”请回答以下问题{latest_question}” message HumanMessage(contentprompt) response llm.invoke([message]) answer response.content # 更新State将回答添加到消息历史并更新final_answer new_messages state[“messages”] [AIMessage(contentanswer)] return {“messages”: new_messages, “final_answer”: answer}2.3 编译并运行图将节点和边组装起来编译成一个可执行的工作流。from langgraph.graph import StateGraph, END # 3. 创建图构建器 workflow StateGraph(State) # 4. 添加节点 workflow.add_node(“generate”, generate_question) workflow.add_node(“answer”, answer_question) # 5. 设置边执行顺序 workflow.set_entry_point(“generate”) # 入口节点 workflow.add_edge(“generate”, “answer”) # generate执行完后去answer workflow.add_edge(“answer”, END) # answer执行完后结束 # 6. 编译图 app workflow.compile() # 7. 运行工作流 initial_state {“messages”: [], “final_answer”: “”} final_state app.invoke(initial_state) print(“最终答案”, final_state[“final_answer”]) print(“完整消息历史”, final_state[“messages”])运行成功标志没有报错。控制台打印出了一个关于Python的问题和对应的答案。你理解了State如何在节点间流动。如果这一步卡住90%的原因是API Key未设置或错误检查OPENAI_API_KEY环境变量。网络问题确保能访问LLM API。版本不兼容langgraph和langchain的版本不匹配回退到更稳定的版本组合试试。3. 接入Langfuse给你的工作流装上可观测性现在你的基础工作流能跑了但它是个“黑盒”。接下来接入Langfuse看看里面到底发生了什么。3.1 部署或连接Langfuse选项A使用Langfuse Cloud最快去 langfuse.com 注册一个账号创建一个新项目。在项目设置里获取你的LANGFUSE_SECRET_KEYLANGFUSE_PUBLIC_KEY和LANGFUSE_HOST。选项B本地部署数据可控克隆Langfuse仓库使用Docker Compose启动需要安装Docker。默认会在本地启动服务前端、后端、数据库访问http://localhost:3000。初始账号密码在文档中首次登录后同样获取上述密钥。3.2 在LangGraph中集成Langfuse回调修改上面的代码在调用工作流时传入Langfuse的回调处理器。from langfuse.callback import CallbackHandler # 初始化Langfuse回调处理器 langfuse_handler CallbackHandler( secret_key“your-langfuse-secret-key”, public_key“your-langfuse-public-key”, host“https://cloud.langfuse.com” # 如果是Cloud。本地部署则为 “http://localhost:3000 ) # 在invoke时传入callbacks参数 final_state app.invoke( initial_state, config{“callbacks”: [langfuse_handler], “configurable”: {“thread_id”: “test-run-1”}} )运行这段代码后打开你的Langfuse控制台Cloud或本地你应该能在“Traces”页面看到一次执行记录。点进去可以看到Trace代表一次完整的app.invoke调用。SpansTrace下的子步骤。理想情况下你应该能看到generate和answer两个节点作为独立的Span。Observations更细粒度的记录比如每次LLM调用llm.invoke都会被记录包括输入、输出、token用量、耗时。集成成功标志Langfuse控制台有数据且没有报错。你能清晰地看到工作流中两个节点的执行顺序和耗时。每个LLM调用的具体输入输出都能被审查。常见问题看不到数据检查密钥和Host是否正确检查网络是否能连通Langfuse服务器检查回调处理器是否被正确传入。Span层级不对可能需要手动在节点函数内使用langfuseSDK创建Span以获得更清晰的视图。这就是搜索热词中提到的“decorators”可能发挥作用的地方某些版本langfuse提供了装饰器来自动包装函数。数据延迟Langfuse客户端默认是异步批量发送数据可能会有几秒延迟。4. 构建更真实的工作流引入工具、路由与循环简单线性流没问题后我们来构建一个更贴近实际的智能体工作流一个能根据用户问题决定是直接回答还是需要联网搜索的助手。4.1 扩展State与定义工具from typing import Literal from langchain_community.tools import TavilySearchResults from langchain_core.tools import tool # 定义更丰富的State class AgentState(TypedDict): messages: Annotated[list, add_messages] # 新增用户当前问题 question: str # 新增是否需要搜索 need_search: bool # 新增搜索到的信息 search_results: str # 定义一个模拟的搜索工具实际使用时替换为真实的Tavily、Serper等 tool def web_search(query: str) - str: 执行网络搜索。 # 这里是模拟真实情况需要调用API print(f”[模拟搜索] 搜索词{query}“) return f”关于{query}的模拟搜索结果...此处省略具体内容“4.2 创建决策节点路由这是LangGraph的精华——根据State内容动态决定下一步走向。def should_search(state: AgentState) - Literal[“answer_directly”, “search_web”]: 判断节点根据问题决定是否需要搜索 question state[“question”] # 简单的关键词判断逻辑实际可用LLM判断 need_search_keywords [“最新”, “新闻”, “2024”, “价格”, “天气”] for kw in need_search_keywords: if kw in question: return “search_web” return “answer_directly” def search_node(state: AgentState) - AgentState: 搜索节点调用工具获取信息 query state[“question”] results web_search.invoke(query) return {“search_results”: results} def answer_directly_node(state: AgentState) - AgentState: 直接回答节点使用LLM基于对话历史回答 history state[“messages”] # 这里简化处理实际可以构造更复杂的提示词 response llm.invoke(history) new_messages state[“messages”] [response] return {“messages”: new_messages} def answer_with_search_node(state: AgentState) - AgentState: 结合搜索回答节点将搜索结果作为上下文给LLM history state[“messages”] search_info state[“search_results”] prompt f”基于以下信息回答问题{search_info}\n\n问题{state[question]}” human_msg HumanMessage(contentprompt) response llm.invoke([human_msg]) new_messages state[“messages”] [response] return {“messages”: new_messages}4.3 组装有条件分支的图from langgraph.graph import START # 创建新图 agent_workflow StateGraph(AgentState) # 添加节点 agent_workflow.add_node(“should_search”, should_search) # 注意这是一个函数返回的是边名不是State agent_workflow.add_node(“search”, search_node) agent_workflow.add_node(“answer_directly”, answer_directly_node) agent_workflow.add_node(“answer_with_search”, answer_with_search_node) # 设置入口和条件边 agent_workflow.set_entry_point(“should_search”) # should_search节点的输出是边名”answer_directly”或”search_web”据此路由 agent_workflow.add_conditional_edges( “should_search”, should_search, # 路由函数 { “answer_directly”: “answer_directly”, “search_web”: “search”, } ) # 搜索完成后流向answer_with_search节点 agent_workflow.add_edge(“search”, “answer_with_search”) # 两个回答节点都流向END agent_workflow.add_edge(“answer_directly”, END) agent_workflow.add_edge(“answer_with_search”, END) # 编译 agent_app agent_workflow.compile() # 运行测试 test_state_1 {“messages”: [], “question”: “Python的列表怎么用”, “need_search”: False, “search_results”: “”} test_state_2 {“messages”: [], “question”: “今天北京天气怎么样”, “need_search”: False, “search_results”: “”} result1 agent_app.invoke(test_state_1, config{“callbacks”: [langfuse_handler]}) result2 agent_app.invoke(test_state_2, config{“callbacks”: [langfuse_handler]}) print(“问题1直接回答流程”, [msg.content for msg in result1[“messages”]]) print(“问题2搜索后回答流程”, [msg.content for msg in result2[“messages”]])现在去Langfuse控制台查看这两次执行test_state_1和test_state_2。你应该能看到清晰的分支轨迹一次直接走了answer_directly另一次走了search-answer_with_search。这证明了你的工作流具备了基本的决策能力。5. 模型层优化何时以及如何引入SFTTrainer和QAT前面我们都在用现成的API模型如GPT-3.5。当你有特定需求时就需要动模型本身了。5.1 使用SFTTrainer进行领域微调场景你的LangGraph智能体专门处理医疗报告需要模型理解大量医学术语和固定格式。步骤准备数据整理成(instruction, input, output)格式的JSONL文件。选择基座模型如Qwen2-7B-Instruct,Llama-3-8B-Instruct。配置训练参数使用QLoRA等高效微调技术以减少资源需求。使用SFTTrainer训练。# 这是一个高度简化的示例框架真实训练需要大量配置 from datasets import load_dataset from transformers import AutoTokenizer, AutoModelForCausalLM from trl import SFTTrainer, SFTConfig from peft import LoraConfig # 1. 加载数据和模型 dataset load_dataset(“json”, data_files“medical_finetune_data.jsonl”, split“train”) model_id “Qwen/Qwen2-7B-Instruct” tokenizer AutoTokenizer.from_pretrained(model_id) model AutoModelForCausalLM.from_pretrained(model_id, device_map“auto”) # 2. 配置LoRA peft_config LoraConfig( r16, lora_alpha32, target_modules[“q_proj”, “k_proj”, “v_proj”, “o_proj”], lora_dropout0.05, bias“none”, task_type“CAUSAL_LM” ) # 3. 配置训练参数 training_args SFTConfig( output_dir“./results”, num_train_epochs3, per_device_train_batch_size4, gradient_accumulation_steps4, learning_rate2e-4, fp16True, logging_steps10, save_steps500, max_seq_length1024, ) # 4. 初始化Trainer trainer SFTTrainer( modelmodel, argstraining_args, train_datasetdataset, peft_configpeft_config, tokenizertokenizer, formatting_funcformat_instruction # 你需要定义这个函数来处理数据格式 ) # 5. 训练 trainer.train()训练后你会得到一个适配器Adapter或完整的微调模型。在LangGraph中你可以用HuggingFacePipeline或类似方式加载这个本地模型替换掉原来的ChatOpenAI节点。关键判断微调是否有效你需要一个独立的验证集用Langfuse来追踪和对比微调模型与原始模型在相同测试用例上的表现回答质量、相关性。5.2 使用量化感知训练QAT准备边缘部署场景你需要将包含微调模型的智能体部署到Jetson Orin等边缘设备内存和算力有限。重要前提QAT需要在训练阶段进行。如果你已经有一个训练好的模型无论是原始模型还是SFT后的模型你需要有一个代表校准数据集来模拟量化过程。简化流程准备模型加载你的FP32模型。准备QAT配置指定哪些层需要量化以及量化方案如INT8。校准在训练数据的一个子集上运行前向传播收集激活值的统计信息用于确定量化的缩放比例。微调在模拟量化的模式下进行少量轮次的训练让模型适应精度损失。导出导出为量化后的模型格式如PyTorch的量化模型、ONNX量化模型或TensorRT引擎。# 这是一个概念性示例实际代码依赖具体库如Intel的ITREX或NVIDIA的TAO import torch from torch.ao.quantization import QuantStub, DeQuantStub, default_qconfig, prepare_qat, convert # 1. 定义插入量化/反量化桩的模型 class QATReadyModel(torch.nn.Module): def __init__(self, original_model): super().__init__() self.quant QuantStub() self.dequant DeQuantStub() self.model original_model def forward(self, x): x self.quant(x) x self.model(x) x self.dequant(x) return x # 2. 包装模型 fp32_model … # 你的训练好的模型 qat_model QATReadyModel(fp32_model) qat_model.train() # 3. 准备QAT qat_model.qconfig torch.ao.quantization.get_default_qat_qconfig(‘fbgemm’) # 或 ‘qnnpack’ torch.ao.quantization.prepare_qat(qat_model, inplaceTrue) # 4. 校准/微调简化仅用少量数据做前向传播 calibration_data … # 你的校准数据 qat_model.eval() with torch.no_grad(): for data in calibration_data: _ qat_model(data) # 切换回训练模式进行微调 qat_model.train() # … 进行少量迭代的训练 … # 5. 转换为量化模型 quantized_model torch.ao.quantization.convert(qat_model.eval(), inplaceFalse) # 保存 quantized_model核心挑战工具链复杂Transformer模型的QAT工具链如optimum-intel,torch.ao.quantization仍在快速发展需要仔细查阅对应版本的文档。硬件相关在Jetson上部署最终可能需要转换为TensorRT引擎并使用其特定的量化工具。精度评估必须在验证集上严格对比QAT后模型与原始模型的精度确保损失在可接受范围内。给你的建议在LangGraph项目初期不要过早陷入QAT的细节。先用API或FP16模型把工作流和业务逻辑跑通。当性能评估通过Langfuse确认模型调用是瓶颈且部署目标硬件明确时再启动QAT相关工作。6. 生产化考量从脚本到可靠服务当你完成了原型验证接下来需要考虑如何让这个系统稳定运行。6.1 状态State持久化与多会话上面的例子中State是内存对象。生产环境需要将会话状态特别是messages历史保存到数据库如Redis、PostgreSQL。LangGraph支持自定义Checkpointer来实现这一点。你需要将State的序列化/反序列化与你的存储方案结合。6.2 异步、队列与并发app.invoke是同步调用。对于高并发请求你需要将LangGraph工作流包装成异步函数。使用任务队列如Celery、Dramatiq或Redis Queue来管理请求避免阻塞Web服务器。为每个任务配置独立的configurable参数如thread_id以便在Langfuse中区分。6.3 全面的可观测性与评估Langfuse不仅用于调试还可用于评分Score为每次Trace自动或手动打分例如回答相关性1-5分。数据集Dataset将重要的测试用例保存为数据集用于回归测试。提示词管理Prompts将不同节点的提示词模板存储在Langfuse实现版本管理和A/B测试。告警Alerting监控耗时、错误率、成本等指标设置阈值告警。6.4 配置管理与版本控制将工作流的结构图定义、提示词模板、模型配置、工具API密钥等全部外部化使用配置文件、环境变量或配置中心。确保能轻松回滚到任何一个稳定版本。7. 常见问题排查清单当你的LangGraph应用出现问题时按这个顺序排查工作流根本跑不起来检查点Python和包版本是否匹配requirements.txt是否准确检查点LLM API密钥或本地模型路径是否正确网络是否通畅检查点虚拟环境是否激活工作流能跑但逻辑错误走错分支、结果不对检查点打开Langfuse仔细查看每个节点的输入State和输出State。问题往往出在路由节点的判断逻辑或某个节点的处理逻辑上。检查点你的State定义是否涵盖了所有必要信息类型注解是否正确检查点条件边add_conditional_edges的返回值和边名映射是否正确Langfuse看不到数据检查点回调处理器是否在invoke时传入config参数是否正确检查点Langfuse服务器地址和密钥是否正确本地部署时服务是否健康检查点客户端是否有网络权限尝试在代码中捕获并打印langfuse_handler的异常。性能瓶颈速度慢检查点在Langfuse中分析哪个Span耗时最长。通常是LLM调用。解决方案考虑使用更快的模型、启用API的流式响应以减少感知延迟、或引入缓存对于重复性问题。解决方案对于本地模型考虑前面提到的量化QAT和优化。微调或量化后效果变差检查点你的训练/校准数据是否有代表性数据质量是第一位。检查点验证集上的评估是否充分必须用未见过的数据评估。检查点QAT的校准数据是否来自真实数据分布量化参数是否合理行动在Langfuse中创建同一个测试数据集分别追踪原始模型和优化后模型的Trace进行逐条对比。这个组合技术栈功能强大但复杂度也不低。我的建议始终是分而治之逐步验证。先让最简单的LangGraph流跑通再接入观测然后优化模型最后处理部署。每一步都确保站稳了再走下一步并用Langfuse作为你每一步的“眼睛”和“尺子”这样能最大程度减少后期调试的混乱。
返回列表