☰
LangChain 单元测试怎么写:Fake LLM、Mock 与录制回放
2026/10/12 0:15:47 网站建设 项目流程
个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <

文章目录

  • LangChain 单元测试怎么写:Fake LLM、Mock 与录制回放
    • 一、测试策略:分三层
    • 二、FakeListLLM:按顺序返回预设答案
    • 三、FakeChatModel:模拟对话模型
    • 四、Fake 的配套组件
      • 4.1 FakeEmbeddings
      • 4.2 FakeRetriever
      • 4.3 GenericFakeChatModel:模拟流式
    • 五、怎么验证"prompt 里有什么"
    • 六、用 Mock 验证"调用了几次"
    • 七、录制回放:一次性拿到真实响应
    • 八、什么该真跑
    • 九、一份 pytest 组织建议
    • 十、常见坑
    • 十一、小结

LangChain 单元测试怎么写:Fake LLM、Mock 与录制回放

LLM 应用难测试:输出不确定、调用要钱、还慢。
于是很多人干脆不写测试,改一行 prompt 全靠手动试。

这篇讲怎么用Fake LLM把测试做成"秒级、免费、可重复",
以及哪些该 mock、哪些该真跑。

一、测试策略:分三层

层测什么用不用真模型速度
单元测试链的结构、格式、路由分支不用(Fake LLM)毫秒
集成测试真实调用能否跑通用(少量)秒级
效果评测答案质量用(测试集)分钟级

90% 的测试应该在第一层——免费、快、稳定。
只有少量用例放第二三层。

二、FakeListLLM:按顺序返回预设答案

fromlangchain_community.llms.fakeimportFakeListLLMfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParser llm=FakeListLLM(responses=["答案A","答案B","答案C"])print(llm.invoke("第一个问题"))# 答案Aprint(llm.invoke("第二个问题"))# 答案Bprint(llm.invoke("第三个问题"))# 答案Cprint(llm.invoke("第四个问题"))# 答案A(循环)

它不看输入,按顺序吐答案。
这正好适合测试"链的结构对不对":

deftest_chain_structure():llm=FakeListLLM(responses=["最终结果"])chain=(ChatPromptTemplate.from_template("问题:{q}")|llm|StrOutputParser())result=chain.invoke({"q":"任意问题"})assertresult=="最终结果"

这个测试验证的是"链能跑通、输出能解析",
至于答案对不对,不是这一层要管的。

三、FakeChatModel:模拟对话模型

需要返回AIMessage(比如测试工具调用)时用它:

fromlangchain_community.chat_models.fakeimportFakeMessagesListChatModelfromlangchain_core.messagesimportAIMessage model=FakeMessagesListChatModel(responses=[AIMessage(content="你好"),AIMessage(content="",tool_calls=[{"name":"get_weather","args":{"city":"北京"},"id":"call_1"}],),])

测试工具调用链路特别有用:
不用真调模型就能验证"我的循环能不能正确执行工具、把结果塞回去"。

deftest_tool_loop():model=FakeMessagesListChatModel(responses=[AIMessage(content="",tool_calls=[{"name":"search","args":{"q":"test"},"id":"c1"}]),AIMessage(content="最终答案"),])agent=build_agent(model,tools=[search_tool])result=agent.invoke({"messages":[("user","查一下test")]})assert"最终答案"inresult["messages"][-1].content

四、Fake 的配套组件

除了 LLM,其他依赖也能 fake:

4.1 FakeEmbeddings

fromlangchain_community.embeddingsimportFakeEmbeddings emb=FakeEmbeddings(size=384)vectors=emb.embed_documents(["a","b"])print(len(vectors[0]))# 384

它的向量是基于文本哈希生成的,
所以同样的文本永远得到同样的向量——
这让向量检索相关的测试变得可重复。

deftest_retrieval():vs=FAISS.from_documents(docs,FakeEmbeddings(size=128))results=vs.similarity_search("某个问题",k=3)assertlen(results)==3

4.2 FakeRetriever

fromlangchain_core.retrieversimportFakeRetrieverfromlangchain_core.documentsimportDocument retriever=FakeRetriever(documents=[Document(page_content="预设文档1",metadata={"source":"a.md"}),Document(page_content="预设文档2",metadata={"source":"b.md"}),])docs=retriever.invoke("任何问题")# 永远返回这两条

测试 RAG 链的组装逻辑时非常好用——
你只想验证"检索结果有没有正确拼进 prompt",
不想真的建索引。

4.3 GenericFakeChatModel:模拟流式

fromlangchain_community.chat_models.fakeimportGenericFakeChatModelfromlangchain_core.messagesimportAIMessageChunk model=GenericFakeChatModel(messages=lambda:AIMessageChunk(content="逐字"))

可以自定义生成逻辑,用来测试流式处理。

五、怎么验证"prompt 里有什么"

很多时候你想测的是:「检索到的文档有没有被正确拼进 prompt」。
这时不需要看最终输出,直接把 prompt 渲染出来:

deftest_context_in_prompt():prompt=ChatPromptTemplate.from_messages([("system","只依据上下文回答。\n\n上下文:\n{context}"),("human","{question}"),])messages=prompt.format_messages(context="【文档1】二线城市 450 元",question="住宿标准?",)text=messages[0].contentassert"450 元"intextassert"只依据上下文"intext

这是最实用的一类测试——快、稳、直击要害。
比"调用真模型看它答得对不对"高效得多。

六、用 Mock 验证"调用了几次"

有些逻辑要验证的是调用行为:

fromunittest.mockimportMagicMockdeftest_calls_llm_once():mock_llm=MagicMock()mock_llm.invoke.return_value="结果"chain=prompt|mock_llm|StrOutputParser()chain.invoke({"q":"x"})assertmock_llm.invoke.call_count==1deftest_no_call_when_cached():mock_llm=MagicMock()mock_llm.invoke.return_value="结果"cached=CachedChain(mock_llm)cached.invoke({"q":"x"})cached.invoke({"q":"x"})# 同样的输入assertmock_llm.invoke.call_count==1# 第二次应命中缓存

测缓存、测重试、测降级,用 Mock 数调用次数是标准做法。

七、录制回放:一次性拿到真实响应

有时候你既想要真实响应,又不想每次都真调。
办法是录一次,之后回放:

importjsonfrompathlibimportPath FIXTURES=Path("tests/fixtures")classRecordReplayLLM:"""有 fixture 就回放,没有就真调并录下来"""def__init__(self,real_llm,name):self.real=real_llm self.path=FIXTURES/f"{name}.json"definvoke(self,prompt,**kw):key=str(prompt)[:200]data=json.loads(self.path.read_text(encoding="utf-8"))ifself.path.exists()else{}ifkeyindata:returndata[key]result=self.real.invoke(prompt,**kw)data[key]=result self.path.write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding="utf-8")returnresult

第一次跑真调用并落盘,之后全部走 fixture——免费且稳定。

使用时注意:

  • fixture 要提交到 git,团队共享;
  • prompt 改了要删掉旧 fixture 重录,否则测的是过时的响应;
  • 敏感数据别录进去。

八、什么该真跑

单元测试覆盖不到的,留少量真跑:

importpytest@pytest.mark.slow# 标记为慢测试,CI 可选跳过deftest_real_end_to_end():"""验证真实模型能跑通,不验证答案质量"""result=real_chain.invoke({"question":"报销标准?"})assertisinstance(result,str)assertlen(result)>0# 只断言"能跑通不断言内容对不对"

真跑的测试只验证"不崩",不验证"答对"——
后者属于效果评测(用测试集打分,见评测那篇)。

九、一份 pytest 组织建议

tests/ ├── conftest.py # 共享的 fake 组件 ├── fixtures/ │ └── llm_responses.json # 录制的响应 ├── test_prompt.py # prompt 渲染(快) ├── test_chain.py # 链结构(Fake LLM) ├── test_tools.py # 工具逻辑 ├── test_agent.py # Agent 流程(Fake ChatModel) └── test_e2e.py # 真跑(标 @pytest.mark.slow)
# conftest.pyimportpytestfromlangchain_community.llms.fakeimportFakeListLLM@pytest.fixturedeffake_llm():returnFakeListLLM(responses=["测试答案"])@pytest.fixturedeffake_retriever():returnFakeRetriever(documents=[Document(page_content="测试文档",metadata={"source":"test.md"}),])

十、常见坑

坑说明
忘记把 fake 换回来上线后返回固定字符串——上线前全局搜 Fake
Fake 顺序搞错FakeListLLM按顺序消费,多调一次就错位
测试断言"内容对不对"那是评测的事,单测只管"结构对不对"
fixture 过期改了 prompt 必须重录
只测 happy path也要测"工具报错""解析失败"这些分支

第一条最危险,建议在 CI 加一条检查:

grep-r"Fake"src/&&echo"生产代码里出现 Fake!"&&exit1||exit0

十一、小结

  • 分三层:单测(Fake,快免费)→ 集成(少量真跑)→ 效果评测(测试集打分);
  • FakeListLLM按顺序吐答案,验证链结构而非内容;
    FakeMessagesListChatModel能模拟tool_calls,测工具循环;
  • 配套:FakeEmbeddings(同文本同向量,可重复)、
    FakeRetriever(固定返回,测拼装逻辑);
  • 验证"prompt 里有什么"用format_messages直接断言,最实用;
  • 测调用行为(缓存/重试/降级)用Mock 数call_count;
  • 想要真实响应又不想每次花钱 →录制回放;
  • ⚠️上线前全局搜一遍 Fake,防止调试组件进生产。

到这里 LangChain 的工程实践部分告一段落。
下一篇进入 RAG 深水区:MultiVectorRetriever。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询