Langchain的LCEL(LangChain Expression Language)提供了简洁的链式语法,让开发者可以快速组合提示模板、模型、输出解析器等组件构建处理流程,但在实际开发过程中,我们常常需要查看链中每个中间步骤的处理结果来排查问题,默认情况下LCEL链不会输出中间步骤的内容,需要手动配置激活相关功能。

LCEL链的基本运行逻辑
LCEL链通过管道操作符|将不同的可运行组件串联起来,上一个组件的输出会作为下一个组件的输入,整个链作为一个整体接收输入并返回最终结果。比如下面这个简单的LCEL链示例,实现了输入问题后先调用提示模板生成提示,再调用大模型生成回答,最后解析输出:
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
# 构建提示模板
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个 helpful 的助手"),
("user", "{question}")
])
# 初始化大模型
model = ChatOpenAI(model="gpt-3.5-turbo")
# 初始化输出解析器
parser = StrOutputParser()
# 构建LCEL链
chain = prompt | model | parser
# 运行链
result = chain.invoke({"question": "什么是LCEL"})
print(result)
这个链运行后只会返回最终的解析结果,我们无法看到prompt生成的提示内容,也无法看到model返回的原始响应,调试时很难定位问题出在哪个环节。
激活中间步骤输出的方法
方法一:使用with_config配置回调
LCEL链支持通过with_config方法配置回调参数,我们可以自定义回调类来捕获每个组件的运行输入和输出,从而实现中间步骤的输出。首先需要定义一个继承自BaseCallbackHandler的回调类,重写对应的方法记录中间数据:
from langchain_core.callbacks import BaseCallbackHandler
class StepOutputHandler(BaseCallbackHandler):
def on_chain_start(self, serialized, inputs, **kwargs):
# 链开始运行时触发,记录输入
print(f"链开始运行,输入参数:{inputs}")
def on_chain_end(self, outputs, **kwargs):
# 链结束运行时触发,记录输出
print(f"链结束运行,输出结果:{outputs}")
def on_llm_start(self, serialized, prompts, **kwargs):
# 大模型开始运行时触发,记录提示内容
print(f"大模型开始运行,提示内容:{prompts}")
def on_llm_end(self, response, **kwargs):
# 大模型结束运行时触发,记录原始响应
print(f"大模型结束运行,原始响应:{response}")
def on_prompt_end(self, formatted_prompt, **kwargs):
# 提示模板生成结束触发,记录生成的提示
print(f"提示模板生成结束,格式化提示:{formatted_prompt}")
然后将这个回调处理器配置到链中,运行链时就会自动输出每个中间步骤的内容:
# 初始化回调处理器
handler = StepOutputHandler()
# 为链配置回调
chain_with_callback = chain.with_config({"callbacks": [handler]})
# 运行带回调的链
chain_with_callback.invoke({"question": "什么是LCEL"})
运行后可以看到每个环节的输入输出信息,方便我们排查每个组件的运行是否符合预期。
方法二:使用RunnablePassthrough传递中间结果
如果我们不需要全局的回调配置,只是临时需要查看某个步骤的输出,可以使用RunnablePassthrough组件将中间结果传递到后续步骤,同时保留原始输入。比如我们需要查看提示模板生成的提示内容,可以这样修改链:
from langchain_core.runnables import RunnablePassthrough
# 构建新的链,在提示模板之后插入RunnablePassthrough
# assign方法可以将中间结果赋值到字典的新键中,同时保留原始输入
chain_with_step = (
prompt
| RunnablePassthrough.assign(middle_output=lambda x: print(f"提示模板输出:{x}") or x)
| model
| parser
)
# 运行链
chain_with_step.invoke({"question": "什么是LCEL"})
这种方式适合临时调试单个环节的输出,不需要额外定义回调类,使用起来更加灵活,但是如果需要查看多个环节的输出,就需要多次插入RunnablePassthrough,代码会相对繁琐。
两种方法的适用场景对比
我们可以通过下面的表格对比两种方法的适用场景,根据实际需求选择合适的方式:
| 对比维度 | 回调配置方法 | RunnablePassthrough方法 |
|---|---|---|
| 适用场景 | 需要全局查看所有中间步骤输出,长期调试使用 | 临时查看单个或少数几个步骤输出,快速验证使用 |
| 代码复杂度 | 需要定义回调类,初始配置稍复杂 | 不需要额外定义类,直接修改链结构即可 |
| 灵活性 | 可以捕获所有组件的运行事件,信息更全面 | 只能查看插入位置的步骤输出,范围有限 |
调试注意事项
在激活中间步骤输出调试时,需要注意以下几点:
- 回调配置方法中的回调类方法会在链运行的每个对应事件触发时执行,如果链运行频率很高,可能会影响运行性能,调试完成后建议移除回调配置。
- 使用
RunnablePassthrough时,assign方法中的lambda表达式如果包含打印逻辑,要确保打印操作不影响原始数据的传递,比如示例中用or x保证打印后返回原始输入。 - 如果链中包含了自定义的可运行组件,回调类需要重写对应的自定义事件方法才能捕获中间输出,具体可以参考Langchain官方回调文档的自定义组件部分。
通过上述两种方法,我们可以轻松激活LCEL链的中间步骤输出,快速定位链运行过程中的问题,提升LCEL相关功能的开发效率。