一上手就把整条链写完,报错时基本没法定位:是模板变量没替换、模型调用超时,还是某个节点的字段名对不上?比较稳的做法是先用最小示例把「提示词模板 + 一次模型调用」跑通,再把多步链一节一节接上,每加一个节点都留一个可回退的检查点,出问题时就停在上一个检查点复现。
适用场景:首次搭 LangChain 链、或链跑出异常结果需要定位。操作动作:单模板单调用先验证输出非空且合理,再按「两步传参 → 统一输入字典 → 条件分支 → 拆步回归」逐个节点接入,每步打印中间结果。验证方式:看打印出来的中间值、键名和分支走向,而不是只看最终文本。风险边界:框架版本之间接口名和报错措辞会有差异,以你本地安装版本为准;调试期不追求一次把链写全。
单独跑通一个提示词模板加模型调用
这一步只验证两件事:模板能不能把变量替换进去,模型能不能返回一段非空文本。模型类换成你环境里实际安装的那个,其余骨架可以照搬。
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_template(
'用一句话解释{concept},不超过30个字。'
)
# 先看模板本身,确认变量被替换
print(prompt.format_messages(concept='提示词模板'))
# 再调模型
model = ChatOpenAI(model='gpt-4o-mini', temperature=0)
resp = model.invoke(prompt.format_messages(concept='提示词模板'))
print(repr(resp.content))
# 接上解析器后得到一个纯字符串
out = (prompt | model | StrOutputParser()).invoke({'concept': '提示词模板'})
print('OUT:', out)
确认输出非空且内容合理,看三点:打印的消息里 {concept} 已经被替换成实际值;resp.content 不是空字符串也不是报错文本;返回内容确实在回答「解释提示词模板」,而不是复述了整段模板。如果模板里变量没被替换,说明占位符写法和传入键名不一致;如果模型侧报认证或连接错误,先解决配置,不要急着往下接链。
把第一步的输出接成第二步的输入
两步链最容易出问题的地方是字段传递:第一步吐出的键名,和第二步模板引用的变量名对不上。先把两步分别跑通,打印中间结果,再拼起来。
prompt1 = ChatPromptTemplate.from_template('用一句话解释{concept}。')
prompt2 = ChatPromptTemplate.from_template('把这句话改写得更口语化:{draft}')
step1 = prompt1 | model | StrOutputParser()
print('step1:', step1.invoke({'concept': '提示词模板'}))
chain = {'draft': step1} | prompt2 | model | StrOutputParser()
print('chain:', chain.invoke({'concept': '提示词模板'}))
{'draft': step1} 这一层的作用就是把上一步的字符串输出包装成一个字典,键名 draft 必须和 prompt2 里引用的名字完全相同。键名写错时,通常表现为 KeyError: 'draft',或者框架抛出的输入校验异常,错误信息里会点名缺失的键、并列出当前实际提供了哪些键。看到这类报错,先改键名,不要改提示词文案。中间结果一定要打印,否则你只看到最终文本,无从判断是哪一步偏了。
用统一的输入字典驱动整条链
每步单独拼参数,时间一长就会出现口径不一致:第一步用「新手」,第二步用「入门用户」。建议在调用处维护一个输入字典,链内所有变量都从这个字典取值。
inputs = {'concept': '提示词模板', 'audience': '新手', 'max_words': 30}
prompt1 = ChatPromptTemplate.from_template(
'面向{audience},用不超过{max_words}字解释{concept}。'
)
prompt2 = ChatPromptTemplate.from_template(
'针对{audience},把下面这句话再简化一次:{draft}'
)
chain = (
{'draft': prompt1 | model | StrOutputParser()}
| {'draft': lambda x: x, **inputs}
| prompt2
| model
| StrOutputParser()
)
print(chain.invoke(inputs))
对应关系要求一一对齐:字典里有 concept、audience、max_words,两个模板里引用的变量名就都必须是这三个中的一个,不能出现同义不同名。字典里多出来的键通常不会报错,但少一个键一定会报缺键。改口径时只改字典这一处,两次运行之间才有可比性。
加入条件分支后确认走到哪一支
分支条件写错的典型表现是「永远只走一条路」,而最终输出看起来还算正常,很难察觉。办法是在每条分支里加一句打印,直接把执行路径暴露出来。
from langchain_core.runnables import RunnableBranch, RunnableLambda
def path_long(x):
print('BRANCH: long, len =', len(x['text']))
return '长文本处理:' + x['text'][:20]
def path_short(x):
print('BRANCH: short, len =', len(x['text']))
return '短文本处理:' + x['text']
branch = RunnableBranch(
(lambda x: len(x['text']) > 40, RunnableLambda(path_long)),
RunnableLambda(path_short),
)
print(branch.invoke({'text': '这是一段用来测试分支的文本'}))
准备两条长度明显不同的输入各跑一次,比较打印出的 BRANCH 标记是否跟着输入变化。如果两条输入都打出同一个标记,说明判断条件恒真或恒假,重点检查比较符方向、字段取值是否为 None,以及最后一项是不是被当成了默认分支。分支内部真正执行的函数也要能被单独调用,方便脱离链验证。
把整条链拆回单步做回归对比
整链结果异常时,用同一份输入分别跑单步和整链,把两边并排记录,比只看最终输出快得多。下面这个记录模板可以直接抄成表格或日志格式。
输入字典:{'concept': '...', 'audience': '...', 'max_words': 30}
step1 单独输出 : ...
step1 链内输出 : ...
step2 单独输出 : ...
step2 链内输出 : ...
整链最终输出 : ...
首个不一致位置 : step1 / step2 / 输出解析
备注 : 是否含换行、是否被截断、字段名是否改动
对比时重点看四类字段:一是各步输出是否为空或只有空白字符;二是同一输入下单步与链内输出是否完全一致,不一致说明传参或分支走了不同路径;三是变量的实际取值有没有被截断、拼接或覆盖;四是键名和返回类型,从字符串变成列表、字典这类类型漂移经常在解析器或分支处发生。定位到首个不一致的位置后,只回退到那一步修改,其余节点保持不动,这样每加一个节点都能知道它有没有带来行为变化。