LangChain 单元测试怎么写:Fake LLM、Mock 与录制回放
个人主页 for_ever_love__ 欢迎各位大佬莅临其他栏目: 大模型开发从0到1 其他栏目: iOS项目总结大全 其他栏目: 我想学python了 其他栏目: iOS UI 文章目录LangChain 单元测试怎么写Fake LLM、Mock 与录制回放一、测试策略分三层二、FakeListLLM按顺序返回预设答案三、FakeChatModel模拟对话模型四、Fake 的配套组件4.1 FakeEmbeddings4.2 FakeRetriever4.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 llmFakeListLLM(responses[答案A,答案B,答案C])print(llm.invoke(第一个问题))# 答案Aprint(llm.invoke(第二个问题))# 答案Bprint(llm.invoke(第三个问题))# 答案Cprint(llm.invoke(第四个问题))# 答案A循环它不看输入按顺序吐答案。这正好适合测试链的结构对不对deftest_chain_structure():llmFakeListLLM(responses[最终结果])chain(ChatPromptTemplate.from_template(问题{q})|llm|StrOutputParser())resultchain.invoke({q:任意问题})assertresult最终结果这个测试验证的是链能跑通、输出能解析至于答案对不对不是这一层要管的。三、FakeChatModel模拟对话模型需要返回AIMessage比如测试工具调用时用它fromlangchain_community.chat_models.fakeimportFakeMessagesListChatModelfromlangchain_core.messagesimportAIMessage modelFakeMessagesListChatModel(responses[AIMessage(content你好),AIMessage(content,tool_calls[{name:get_weather,args:{city:北京},id:call_1}],),])测试工具调用链路特别有用不用真调模型就能验证我的循环能不能正确执行工具、把结果塞回去。deftest_tool_loop():modelFakeMessagesListChatModel(responses[AIMessage(content,tool_calls[{name:search,args:{q:test},id:c1}]),AIMessage(content最终答案),])agentbuild_agent(model,tools[search_tool])resultagent.invoke({messages:[(user,查一下test)]})assert最终答案inresult[messages][-1].content四、Fake 的配套组件除了 LLM其他依赖也能 fake4.1 FakeEmbeddingsfromlangchain_community.embeddingsimportFakeEmbeddings embFakeEmbeddings(size384)vectorsemb.embed_documents([a,b])print(len(vectors[0]))# 384它的向量是基于文本哈希生成的所以同样的文本永远得到同样的向量——这让向量检索相关的测试变得可重复。deftest_retrieval():vsFAISS.from_documents(docs,FakeEmbeddings(size128))resultsvs.similarity_search(某个问题,k3)assertlen(results)34.2 FakeRetrieverfromlangchain_core.retrieversimportFakeRetrieverfromlangchain_core.documentsimportDocument retrieverFakeRetriever(documents[Document(page_content预设文档1,metadata{source:a.md}),Document(page_content预设文档2,metadata{source:b.md}),])docsretriever.invoke(任何问题)# 永远返回这两条测试 RAG 链的组装逻辑时非常好用——你只想验证检索结果有没有正确拼进 prompt不想真的建索引。4.3 GenericFakeChatModel模拟流式fromlangchain_community.chat_models.fakeimportGenericFakeChatModelfromlangchain_core.messagesimportAIMessageChunk modelGenericFakeChatModel(messageslambda:AIMessageChunk(content逐字))可以自定义生成逻辑用来测试流式处理。五、怎么验证prompt 里有什么很多时候你想测的是「检索到的文档有没有被正确拼进 prompt」。这时不需要看最终输出直接把 prompt 渲染出来deftest_context_in_prompt():promptChatPromptTemplate.from_messages([(system,只依据上下文回答。\n\n上下文\n{context}),(human,{question}),])messagesprompt.format_messages(context【文档1】二线城市 450 元,question住宿标准,)textmessages[0].contentassert450 元intextassert只依据上下文intext这是最实用的一类测试——快、稳、直击要害。比调用真模型看它答得对不对高效得多。六、用 Mock 验证调用了几次有些逻辑要验证的是调用行为fromunittest.mockimportMagicMockdeftest_calls_llm_once():mock_llmMagicMock()mock_llm.invoke.return_value结果chainprompt|mock_llm|StrOutputParser()chain.invoke({q:x})assertmock_llm.invoke.call_count1deftest_no_call_when_cached():mock_llmMagicMock()mock_llm.invoke.return_value结果cachedCachedChain(mock_llm)cached.invoke({q:x})cached.invoke({q:x})# 同样的输入assertmock_llm.invoke.call_count1# 第二次应命中缓存测缓存、测重试、测降级用 Mock 数调用次数是标准做法。七、录制回放一次性拿到真实响应有时候你既想要真实响应又不想每次都真调。办法是录一次之后回放importjsonfrompathlibimportPath FIXTURESPath(tests/fixtures)classRecordReplayLLM:有 fixture 就回放没有就真调并录下来def__init__(self,real_llm,name):self.realreal_llm self.pathFIXTURES/f{name}.jsondefinvoke(self,prompt,**kw):keystr(prompt)[:200]datajson.loads(self.path.read_text(encodingutf-8))ifself.path.exists()else{}ifkeyindata:returndata[key]resultself.real.invoke(prompt,**kw)data[key]result self.path.write_text(json.dumps(data,ensure_asciiFalse,indent2),encodingutf-8)returnresult第一次跑真调用并落盘之后全部走 fixture——免费且稳定。使用时注意fixture 要提交到 git团队共享prompt 改了要删掉旧 fixture 重录否则测的是过时的响应敏感数据别录进去。八、什么该真跑单元测试覆盖不到的留少量真跑importpytestpytest.mark.slow# 标记为慢测试CI 可选跳过deftest_real_end_to_end():验证真实模型能跑通不验证答案质量resultreal_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.fakeimportFakeListLLMpytest.fixturedeffake_llm():returnFakeListLLM(responses[测试答案])pytest.fixturedeffake_retriever():returnFakeRetriever(documents[Document(page_content测试文档,metadata{source:test.md}),])十、常见坑坑说明忘记把 fake 换回来上线后返回固定字符串——上线前全局搜 FakeFake 顺序搞错FakeListLLM按顺序消费多调一次就错位测试断言内容对不对那是评测的事单测只管结构对不对fixture 过期改了 prompt 必须重录只测 happy path也要测工具报错解析失败这些分支第一条最危险建议在 CI 加一条检查grep-rFakesrc/echo生产代码里出现 Fakeexit1||exit0十一、小结分三层单测Fake快免费→ 集成少量真跑→ 效果评测测试集打分FakeListLLM按顺序吐答案验证链结构而非内容FakeMessagesListChatModel能模拟tool_calls测工具循环配套FakeEmbeddings同文本同向量可重复、FakeRetriever固定返回测拼装逻辑验证prompt 里有什么用format_messages直接断言最实用测调用行为缓存/重试/降级用Mock 数call_count想要真实响应又不想每次花钱 →录制回放⚠️上线前全局搜一遍 Fake防止调试组件进生产。到这里 LangChain 的工程实践部分告一段落。下一篇进入 RAG 深水区MultiVectorRetriever。