用 DeepSeek、LangChain 和 pytest 搭建第一个可测试的 LLM 应用
本文是项目的第一阶段主要完成以下内容调用 DeepSeek 兼容接口跑通基础 LLM 链路将模型调用封装成可测试的LLMClient通过依赖注入隔离真实模型使用 Fake LLM 编写不访问网络的单元测试覆盖正常返回、模型超时和非法输入实践一次完整的 TDD 红灯—绿灯—重构过程。本文暂不涉及 Agent 工具调用后续文章再继续搭建真正的 Tool Agent。一、为什么“模型能回答”不等于“模型应用可测试”最初的模型调用通常类似下面这样fromlangchain_openaiimportChatOpenAI llmChatOpenAI(...)responsellm.invoke(付航出生在哪一年)print(response.content)这段代码能帮助我们快速确认 API Key、模型地址和网络是否正常但它有几个明显问题模型对象与业务逻辑紧密耦合测试时难以替换每次运行都会访问真实网络并消耗 Token导入模块时如果直接执行调用会将该模块的顶层代码一起执行产生预期之外的效果很难稳定复现超时等外部服务异常鉴权失败、限流和服务不可用等场景也需要在后续通过测试替身或故障注入进行验证。测试代码应该尽可能做到快速、稳定、可重复。为此需要先把“模型创建”和“模型使用”拆开。二、项目环境与目录结构本文使用的主要环境如下Python 3.11 pytest python-dotenv langchain-openai当前目录结构knowledge-agent-quality/ ├── app/ │ └── llm_client.py ├── tests/ │ └── test_llm.py ├── .env ├── .gitignore └── main.py安装基础依赖python-mpipinstalllangchain-openai python-dotenv pytest建议为项目创建独立的虚拟环境或 Conda 环境避免不同项目之间发生依赖污染。三、安全管理 API Key在项目根目录创建.envDEEPSEEK_API_KEY替换为自己的API_Key不要把真实密钥写进 Python 代码也不要提交到 Git 仓库。.gitignore至少应包含.env __pycache__/ *.py[cod] .pytest_cache/ .vscode/ .venv/ venv/ htmlcov/ .coverage reports/提交前可以执行gitstatus--shortgitdiff--cached重点确认.env和真实 API Key 没有进入暂存区。四、使用依赖注入设计可测试的 LLMClient4.1 什么是依赖注入如果LLMClient在内部直接创建ChatOpenAI它就只能依赖真实模型。依赖注入的思路是模型由外部创建再传给LLMClient。正式运行ChatOpenAI → LLMClient 单元测试FakeLLM → LLMClientLLMClient只要求传入对象具有invoke()方法不需要知道底层究竟是 DeepSeek、其他模型还是真实网络之外的测试替身。4.2 LLMClient 实现app/llm_client.pyclassLLMClient:def__init__(self,llm):self.llmllmdefchat(self,prompt:str)-str:ifnotpromptornotprompt.strip():raiseValueError(prompt不能为空)responseself.llm.invoke(prompt)returnresponse.content这个类只负责三件事接收外部模型依赖校验用户输入调用模型并提取content。这里没有加载.env也没有创建真实模型更没有在模块底部直接发起请求。因此导入该模块不会产生网络调用。4.3 为什么使用if not prompt or not prompt.strip()我们需要拦截以下无效输入None \t\nifnotpromptornotprompt.strip():当prompt是None或空字符串时左侧已经成立不会继续调用strip()当prompt有值时再使用strip()判断它是否只包含空白字符。这里的strip()只用于校验没有修改原始 prompt正常输入仍会原样传给模型。五、在程序入口中创建真实模型main.py负责配置和组装真实依赖importosfromdotenvimportload_dotenvfromlangchain_openaiimportChatOpenAIfromapp.llm_clientimportLLMClientdefmain():load_dotenv()api_keyos.getenv(DEEPSEEK_API_KEY)ifnotapi_key:raiseRuntimeError(DEEPSEEK_API_KEY未配置)chat_modelChatOpenAI(modeldeepseek-v4-flash,api_keyapi_key,base_urlhttps://api.deepseek.com,)clientLLMClient(chat_model)answerclient.chat(付航出生在哪一年)print(answer)if__name____main__:main()这里有两个重要设计。5.1 主动检查配置如果没有读取到 API Key程序主动抛出清晰异常raiseRuntimeError(DEEPSEEK_API_KEY未配置)与print()后直接退出相比抛出异常会让命令返回非零退出码CI 更容易识别失败。5.2 使用入口保护if__name____main__:main()只有直接运行python main.py时才会调用真实模型。其他模块导入main.py时不会自动发送请求。六、使用 Fake LLM 隔离真实模型单元测试的目标不是验证 DeepSeek 服务是否在线而是验证我们自己的代码逻辑。因此可以制作一个与真实模型拥有相同最小接口的 Fake LLMfromtypesimportSimpleNamespaceclassFakeLLM:def__init__(self):self.last_promptNonedefinvoke(self,prompt):self.last_promptpromptreturnSimpleNamespace(content2026年)真实模型与 Fake LLM 的共同接口是invoke(prompt) → 返回具有 content 属性的响应对象SimpleNamespace可以快速构造一个带content属性的假响应不需要引入真实模型响应类。七、第一条正常链路单元测试deftest_chat_passes_prompt_and_returns_content():fake_llmFakeLLM()clientLLMClient(fake_llm)resultclient.chat(测试问题)assertfake_llm.last_prompt测试问题assertresult2026年这条测试验证两个接口契约LLMClient将原始 prompt 正确传给底层模型LLMClient正确提取并返回响应中的content。测试完全不访问网络执行速度通常只有几毫秒也不会消耗 Token。八、模拟模型超时异常场景不应该依赖真实网络偶然失败。我们可以主动构造一个超时模型classTimeoutLLM:definvoke(self,prompt):raiseTimeoutError(模型调用超时)然后使用pytest.raises验证异常类型和消息deftest_chat_propagates_timeout_error():timeout_llmTimeoutLLM()clientLLMClient(timeout_llm)withpytest.raises(TimeoutError,match模型调用超时):client.chat(测试)这条测试固定了当前异常策略底层模型发生超时时LLMClient不吞掉异常而是向上传递给调用方。后续如果增加重试、统一异常封装或降级策略也可以基于这条测试继续演进。九、使用参数化覆盖空输入边界对于None、空字符串、空格、Tab 和换行符如果分别写五个测试函数会产生很多重复代码。pytest 参数化可以让一条测试使用多组数据运行pytest.mark.parametrize(invalid_prompt,[None,, ,\t,\n],)deftest_chat_rejects_empty_prompt(invalid_prompt):fake_llmFakeLLM()clientLLMClient(fake_llm)withpytest.raises(ValueError,matchprompt不能为空):client.chat(invalid_prompt)assertfake_llm.last_promptisNone最后一条断言非常重要assertfake_llm.last_promptisNone它不仅验证程序抛出了异常还验证无效输入在进入底层模型之前就被拦截避免无意义的网络请求和 Token 消耗。十、一次真实的 TDD 过程空输入校验采用了测试驱动开发的方式。10.1 Red先写失败测试最初的LLMClient没有输入校验。新增测试后pytest 报告Failed: DID NOT RAISE ValueError这个失败证明测试准确暴露了尚未实现的需求。10.2 Green增加最小实现首先加入ifnotprompt:raiseValueError(prompt不能为空)空字符串测试通过了但加入空格、Tab 和换行数据后测试再次失败。这说明if not prompt不能识别纯空白字符串。10.3 补充边界发现执行顺序问题一度尝试promptprompt.strip()加入None用例后出现AttributeError: NoneType object has no attribute strip最终实现调整为ifnotpromptornotprompt.strip():raiseValueError(prompt不能为空)最终所有输入边界测试通过。测试全部通过只能证明已经覆盖的场景通过并不代表没有遗漏场景。测试设计的价值不仅是验证代码还在于持续发现需求边界。十一、执行测试运行全部测试python-mpytest-v只运行 LLM 客户端测试python-mpytest tests/test_llm.py-v本阶段的 LLM 客户端测试包含1 条正常调用测试1 条超时异常测试5 组空输入参数化测试。共计 7 个测试用例。提交代码前还可以检查空白格式gitdiff--checkgitdiff--cached--check两条命令分别检查未暂存和已暂存改动中的行尾空格、多余空白行等常见问题。十二、测试分层哪些测试不应该混在一起当前实践中可以区分两类测试。单元测试使用 Fake LLM不访问网络验证自己的代码prompt 是否正确传递 content 是否正确返回 非法输入是否提前拦截 底层异常是否按约定传播特点是快速、稳定、无费用适合每次提交和 CI 回归。集成测试使用真实 API Key 和模型服务验证真实请求是否能够返回 环境变量是否正确 模型地址是否可用 鉴权是否成功集成测试依赖网络并产生费用不应该替代单元测试也不适合在每次本地修改后无条件执行。后续可以通过 pytest marker 将两类测试分开执行。十三、阶段总结这一阶段虽然还没有搭建完整 Agent但已经完成了一个可测试的 LLM 基础层真实模型调用 ↓ 模型创建与使用解耦 ↓ Fake LLM 替代真实网络 ↓ 正常、异常和边界测试 ↓ 形成可重复执行的测试基线对于 Agent 测试来说这一步的意义是建立底层可测性。否则未来加入 RAG、Tool 和 MCP 后一旦最终结果错误很难判断问题来自模型服务、Agent 决策、工具执行还是外围系统。下一阶段将开始构建最小 Tool Agent重点测试Agent 是否选择了正确工具工具参数是否正确工具调用次数是否符合预期无需工具的问题是否发生了错误调用工具异常时 Agent 如何提示或降级。

相关新闻

stm32学习第二天

stm32学习第二天

今天学习第一个片上外设:GPIOp2.1:输出模式分别为通用/复用输出推挽/开漏等四种模式。推挽(PP):向外推电流---0:低电压,1:高电压开漏(OD):向内拉电…

2026/8/4 20:48:37 阅读更多 →
FT232 USB UART板:嵌入式开发的串口调试核心工具与应用指南

FT232 USB UART板:嵌入式开发的串口调试核心工具与应用指南

1. 项目概述:从串口调试到产品原型,一块小板的无限可能如果你玩过单片机、调试过路由器,或者拆解过一些智能硬件,那你大概率见过或者用过一种叫做“USB转TTL串口”的小玩意。这东西个头不大,价格也便宜,但在…

2026/8/4 20:25:45 阅读更多 →
洛雪音乐音源配置终极指南:优化性能与多平台兼容性

洛雪音乐音源配置终极指南:优化性能与多平台兼容性

洛雪音乐音源配置终极指南:优化性能与多平台兼容性 【免费下载链接】lxmusic- lxmusic(洛雪音乐)全网最新最全音源 项目地址: https://gitcode.com/gh_mirrors/lx/lxmusic- 想要在洛雪音乐中获得最佳的音乐体验吗?音源配置是提升播放性能和多平台…

2026/8/3 20:19:41 阅读更多 →

最新新闻

小红书无水印下载器XHS-Downloader:从新手到专家的全场景实战指南

小红书无水印下载器XHS-Downloader:从新手到专家的全场景实战指南

小红书无水印下载器XHS-Downloader:从新手到专家的全场景实战指南 【免费下载链接】XHS-Downloader 小红书(XiaoHongShu、RedNote)链接提取/作品采集工具:提取账号发布、收藏、点赞、专辑作品链接;提取搜索结果作品、用…

2026/8/4 20:52:20 阅读更多 →
绿色工厂能耗台账系统怎么落地?申报机构与工厂分工协作全流程|复评不断档实操指南

绿色工厂能耗台账系统怎么落地?申报机构与工厂分工协作全流程|复评不断档实操指南

青岛智碳未来 智碳能碳管理平台实践观察 摘要:绿色工厂能耗台账系统选得好不好,直接决定申报材料有没有"底账"可查。给申报机构与 B 端工厂的结论是:把台账当成日常管理工具而不是申报季临时文件夹,机构负责标准与辅导…

2026/8/4 20:52:20 阅读更多 →
【限时解密】AI口语进步停滞期突破指南:MIT语音实验室未公开的3.7秒响应延迟优化方案

【限时解密】AI口语进步停滞期突破指南:MIT语音实验室未公开的3.7秒响应延迟优化方案

更多请点击: https://kaifayun.com 第一章:AI口语进步停滞期的本质认知与信号识别 AI口语能力在训练中期常出现看似“平台期”的现象,但这并非能力增长的终结,而是模型语言表征、语音对齐与反馈闭环进入深度重构阶段的自然表现。…

2026/8/4 20:52:20 阅读更多 →
终极指南:如何快速免费将QQ音乐qmcflac格式转换为通用mp3文件

终极指南:如何快速免费将QQ音乐qmcflac格式转换为通用mp3文件

终极指南:如何快速免费将QQ音乐qmcflac格式转换为通用mp3文件 【免费下载链接】qmcflac2mp3 直接将qmcflac文件转换成mp3文件,突破QQ音乐的格式限制 项目地址: https://gitcode.com/gh_mirrors/qm/qmcflac2mp3 你是否曾经从QQ音乐下载了喜欢的歌曲…

2026/8/4 20:52:20 阅读更多 →
【74LS138D+08+86组成高电平显示H、低电平显示L、悬空显示O】2025-6-11

【74LS138D+08+86组成高电平显示H、低电平显示L、悬空显示O】2025-6-11

缘由简易逻辑笔怎么加悬空显示0的功能?_嵌入式-CSDN问答 看电路图用简单逻辑无法实现,看引脚接法就不是原顺序不能显示0,假设三极管集电极有3种状态0、1、中点电压,也要通过比较电路实现3个信号再给组合逻辑去处理,所以…

2026/8/4 20:52:20 阅读更多 →
AMD显卡AI创作的终极解决方案:ComfyUI-Zluda深度解析与实践指南

AMD显卡AI创作的终极解决方案:ComfyUI-Zluda深度解析与实践指南

AMD显卡AI创作的终极解决方案:ComfyUI-Zluda深度解析与实践指南 【免费下载链接】ComfyUI-Zluda The most powerful and modular stable diffusion GUI, api and backend with a graph/nodes interface. Now ZLUDA enhanced for better AMD GPU performance. 项目…

2026/8/4 20:51:19 阅读更多 →

日新闻

AI Agent白手起家26: 使用标准事件驱动大模型实践

AI Agent白手起家26: 使用标准事件驱动大模型实践

纲要 练习目标:掌握大模型标准事件的调用回顾 LangChain 中的核心标准事件 invokestreambatchastream_eventswith_structured_output 环境准备实战代码:多种事件调用对比 同步调用与流式输出批量处理异步事件流监听结构化输出 运行说明与预期结果总结与扩…

2026/8/4 0:00:40 阅读更多 →
dealsea是什么?跨境卖家必知的美国deal站入门指南

dealsea是什么?跨境卖家必知的美国deal站入门指南

说实话,第一次听说美国这个老牌折扣网站的跨境卖家,十个有八个会问同一个问题:这个平台到底是干嘛的?我见过一个做家居出口的朋友,他在亚马逊上月销二十万美金,却从来没用过它。我给他看了首页——一屏一屏…

2026/8/4 0:01:40 阅读更多 →
清华大学重磅EST:植物自导电闪蒸焦耳热600°C/2600°C两步法!稀土超积累植物秒级转化为CeO₂-石墨烯电催化剂!

清华大学重磅EST:植物自导电闪蒸焦耳热600°C/2600°C两步法!稀土超积累植物秒级转化为CeO₂-石墨烯电催化剂!

通讯作者:邓兵、刘建国通讯单位:清华大学DOI:https://doi.org/10.1021/acs.est.6c00603研究背景稀土元素(REEs)是清洁能源技术与电子器件不可或缺的核心原料,然而传统提取方式依赖能耗高、排放大的采矿与强…

2026/8/4 0:01:40 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/4 13:24:41 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/4 11:41:39 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/4 5:26:40 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/4 13:38:24 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/4 11:09:16 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/4 13:38:40 阅读更多 →