ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LangChain实战:基于Agent与RAG自动生成Playwright测试脚本

LangChain实战:基于Agent与RAG自动生成Playwright测试脚本 最近我基于 LangChain 做了一件挺有意思的事让一个 Agent 读取产品测试用例自动生成可执行的 Playwright UI 自动化脚本。整个项目跑下来我最大的感受是——LangChain 这套生态早就不是当年那个只会“拼 Prompt、调接口”的玩具了RAG、Agent、工作流编排这些模块组合起来已经完全能支撑一个真实的生产级场景。这篇内容不是概念科普而是一份实战记录。我会从零开始拆这个项目的完整链路框架怎么选型、本地知识库怎么搭、Agent 怎么设计、Tools 怎么写、LangGraph 怎么编排最后把我在安装配置、检索去重、模型能力对比这些地方踩过的坑也一并列出来。如果你已经把 LangChain 入门资料翻过一遍准备动手做点真实项目那这篇文章应该能帮你省下不少试错时间。1. 项目设计思路别急着写代码先选对框架1.1 这个 Agent 到底要解决什么问题需求方手里有几百条测试用例格式比较杂有 Excel 表格也有 Markdown 文档。以前的做法是测试同学逐条阅读再手动用 Playwright 写脚本一条用例平均要花二三十分钟枯燥而且容易漏断言。我们想做的是把测试用例文件丢给 Agent它自动生成一套结构完整的 UI 自动化脚本测试同学只需要审核和微调。但要直接“文件进、脚本出”其实没那么简单。真实项目里至少有四个坎第一测试用例的描述是非结构化的很多步骤隐含在业务背景里第二历史脚本里有很多约定俗成的写法比如选择器规范、等待策略、断言风格Agent 不可能凭空知道第三生成的脚本必须能通过语法检查和基础逻辑校验不然交出去就是垃圾第四全自动生成的脚本没人敢直接上线必须有人工审核介入。所以这本质上不是一个“调用大模型写代码”的项目而是一个“把已有知识沉淀、检索、生成、校验、人工确认串起来”的系统工程。这也是我坚持要用 LangChain 生态而不是直接裸调 API 的原因我需要可复用的组件也需要可控的流程。1.2 LangChain 与 LangGraph我的取舍标准很多人一上来就纠结 LangChain 和 LangGraph 到底用哪个其实两者根本不是竞争关系而是不同层级的工具。LangChain 更像一个工具集负责对接模型、封装 Prompt、管理向量库、定义 ToolLangGraph 则是一个流程编排框架它把任务状态显式建模成一张图支持条件分支、循环重试、人工介入。打个比方LangChain 是厨房里的锅碗瓢盆和半成品食材LangGraph 是后厨的动线和规矩。你当然可以只用锅碗瓢盆把菜炒出来但一旦工序多了、时不时要做熟度检查、失败了要回锅你就需要一套明确的出餐流程。对比维度LangChainLangGraph定位组件集成与工具库状态化流程编排适合场景固定链路、快速原型分支循环、Agent 多轮决策状态管理隐式传参显式 State 对象可控性中等高可随时人工介入学习曲线较平缓稍陡峭但值得这个项目最终选了“LangChain 做零件LangGraph 做骨架”的组合。具体分工是用 LangChain 的 Tool 机制封装文件读取和脚本校验能力用它的 VectorStore 接口连接 Chroma用它的 PromptTemplate 管理提示词主线流程则用 LangGraph 的 StateGraph 来编排状态里保存测试用例内容、生成的脚本、校验结果、重试次数。想改逻辑的时候改图的节点比改一堆 if-else 的链式调用要舒服得多。2. 本地知识库搭建Ollama Chroma 跑通 RAG 全流程2.1 conda 环境准备与版本锁定先说环境。我习惯用 conda 管理 Python 环境版本选了 Python 3.11。不用 3.12 是因为当时不少基础库的预编译 wheel 还不全在这种依赖众多的项目里能少踩一个坑是一个。conda create -n langchain-agent python3.11 -y conda activate langchain-agent pip install langchain langgraph langchain-community langchain-openai langchain-ollama langchain-chroma chromadb这里必须提醒一句LangChain 的版本演化非常激进0.1 时代的 API 和 1.0 时代的 API 差别很大。比如早期文档里常见的from langchain.llms import Ollama现在已经迁到了langchain-ollama包里直接照抄老代码会报模块不存在的错误。我建议安装完第一时间执行pip list | grep langchain看清主版本并且把版本号写进requirements.txt不要在开发中途随手pip install -U langchain。如果你是要给公司做交付还可以进一步考虑把依赖全部用 pip-tools 或类似方案锁死。这种项目最怕的不是代码难写而是“昨天还能跑今天升级后就不能跑了”的版本漂移。2.2 模型与 Embedding 的选型细节本地模型我用的是 Ollama 拉取的 qwen2.5:7b主要跑在办公内网不涉及外部接口服务还省了数据合规的麻烦。Embedding 模型用的是 nomic-embed-text考虑到后续要检索中文测试文档如果你想在同等资源下追求更好的中文语义效果可以考虑改用 bge-m3或者 mxbai-embed-large。注意 Embedding 选型和主 LLM 是两回事很多新手在“本地化”上犯的第一个错误就是只把聊天模型本地化Embedding 还在用在线接口导致向量入库和检索阶段就产生了隐私瓶颈。ollama pull qwen2.5:7b ollama pull nomic-embed-text拉取耗时取决于网络但本地推理的部署方式几乎没有额外成本。我在 64GB 内存的 Mac 上实测qwen2.5:7b 跑轻量代码生成完全不卡如果用 CPU 推理的 Windows 机器建议把模型降到 qwen2.5:3b 或使用带量化参数的模型牺牲一点效果换可用性。2.3 Chroma 向量库与切分策略Chroma 是最适合这类单体项目的向量库它不需要单独起服务代码里直接持久化到本地目录就行。实现里我用langchain-chroma的Chroma.from_documents把切分好的文档写入磁盘后续查询走as_retriever接口。这里的关键决策是切分策略。文档切分不是简单按固定长度切中文场景尤其要注意标点符号。我使用RecursiveCharacterTextSplitter并显式指定了中文句读的切分点避免把一句话硬生生截成两段导致语义破碎chunk_size 设为 512chunk_overlap 设为 64。为什么是这组参数512 个字符对大多数 QA 场景几乎是安全上限太长会让召回结果包含过多噪音太长则上下文碎片化64 的 overlap 则保证了句子边界的连续性。如果面对的是测试用例这类强结构化文档更好的方案是先用自定义解析器按表格行或 Markdown 标题来切而不是无脑递归切。2.4 MultiVectorRetriever 的高级召回用法在这个项目里我处理历史脚本库时用到了MultiVectorRetriever。这个组件解决的问题很典型一个长文档的某个局部片段语义上可能和用户问题关联不大导致召回失败。但如果你为整个文档生成一段摘要、并用摘要向量去召回再把同文档的原始片段取出来供大模型阅读命中率就会有明显提升。简单说MultiVectorRetriever 维护了两套存储一套是摘要向量库负责“找得对”一套是原始文档存储负责“读得全”。代码结构类似这样from langchain.retrievers.multi_vector import MultiVectorRetriever from langchain.storage import InMemoryStore from langchain_chroma import Chroma vectorstore Chroma(collection_namesummary, embedding_functionembeddings) docstore InMemoryStore() retriever MultiVectorRetriever( vectorstorevectorstore, docstoredocstore, id_keydoc_id, ) # 写入时先为每个大块生成摘要摘要进 vectorstore原块进 docstore # 查询时summary 向量召回后通过 doc_id 映射取回原块实际体验下来对于那种“几百行历史脚本混在一个文件里”的场景这个模式比纯 chunk 向量召回的表现稳定得多。代价是你得多跑一轮摘要生成但换来的是检索质量的大幅提升值得。2.5 完整 RAG 查询链路RAG 流程在项目里的作用是给 Agent 提供“历史相似脚本”的参考。整体链路是测试用例文件进入后先拆成结构化描述然后从历史脚本库检索相似片段把检索结果喂给生成模型的 Prompt最后把生成的脚本写入状态。核心查询链路的代码骨架如下from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough prompt ChatPromptTemplate.from_messages([ (system, 你是资深测试开发请参考历史脚本风格生成 Playwright 代码。), (user, 历史参考\n{context}\n\n测试用例{question}), ]) def format_docs(docs): return \n\n---\n\n.join(d.text for d in docs) qa_chain ( {context: retriever | format_docs, question: RunnablePassthrough()} | prompt | llm | StrOutputParser() )你不需要把 RAG 直接挂进生成的上下文里而是把它封装成一个“检索历史脚本”的工具函数让 Agent 在需要时自行调用。这么做的好处是模型可以先看测试用例、判断是否需要参考历史脚本、再决定检索的关键词。这比“不管三七二十一先检索再生成”要灵活得多也更符合真实的开发节奏。3. Agent 落地从测试用例自动生成 Playwright 脚本3.1 整体流程怎么设计Agent 的主流程我设计成了五个阶段加载用例、检索参考、生成脚本、校验修正、人工确认。这五个阶段不是简单的顺序执行而是带反馈回路的图。生成的脚本如果校验出语法问题就回到生成阶段重写人工审核不通过也会回到生成阶段并附带修改意见。这里有一个经验不要在系统里一次性塞太多自由发挥空间。Agent 的任务粒度越细结果越可控。我先让 Agent 做“阅读理解”把用例拆成步骤列表再专门做“代码生成”最后做“自我校验”。这看起来多了一个步骤实际却大幅降低了生成脚本的幻觉概率——因为模型一旦把动作步骤拆得足够清楚代码生成就不容易漏掉中间步骤。3.2 Tools把文件和校验能力暴露给模型LangChain 的 Tool 机制是 Agent 能力的外延。我们这里给模型注册了三个核心工具一个是读取并解析测试用例文件一个是检索历史脚本一个是校验生成脚本的语法和关键结构。定义方式用tool装饰器即可from langchain.tools import tool tool def load_test_case(file_path: str) - str: 读取测试用例文件支持 xlsx/md/txt返回格式化的步骤列表。 # 按扩展名分发解析逻辑xlsx 用 openpyxl 逐行读取 return formatted_cases tool def search_example_scripts(description: str) - list[str]: 根据步骤描述检索历史 Playwright 脚本片段。 docs retriever.invoke(description, k3) return [d.page_content for d in docs] tool def check_playwright_script(script: str) - str: 对脚本做 ast 语法检查和关键 API 检查返回问题和修复建议。 issues [] tree ast.parse(script) # 检查 page.locator、expect 等关键调用是否出现 return \n.join(issues) if issues else 通过工具返回信息一定要结构化。很多 Agent 项目之所以不稳定是因为工具返回一堆无格式文本模型根本不知道哪些是要害。像check_playwright_script就应该返回“问题行号 问题类型 修复建议”而不是笼统的一句“脚本有问题”。3.3 LangGraph 编排失败重试与人工介入整个主流程我写在StateGraph里。状态对象用TypedDict定义保存测试用例内容、参考脚本、最终脚本、校验反馈、尝试次数这些字段。节点之间通过状态传递数据条件边负责决定下一步往哪走。from typing import TypedDict from langgraph.graph import StateGraph, START, END class AgentState(TypedDict): test_case: str examples: str script: str validation: str attempts: int def load_node(state: AgentState): cases load_test_case.invoke(state[test_case]) return {test_case: cases} def retriever_node(state: AgentState): examples search_example_scripts.invoke(state[test_case]) return {examples: examples} def generate_node(state: AgentState): prompt_text gen_prompt.format(casesstate[test_case], examplesstate[examples]) script llm.invoke(prompt_text) return {script: script, attempts: state.get(attempts, 0) 1} def validate_node(state: AgentState): result check_playwright_script.invoke(state[script]) return {validation: result} graph StateGraph(AgentState) graph.add_node(load, load_node) graph.add_node(retrieve, retriever_node) graph.add_node(generate, generate_node) graph.add_node(validate, validate_node) graph.add_edge(START, load) graph.add_edge(load, retrieve) graph.add_edge(retrieve, generate) graph.add_edge(generate, validate) graph.add_conditional_edges( validate, lambda s: generate if 未通过 in s[validation] and s[attempts] 3 else end, {generate: generate, end: END} )人工确认节点我再单独用一个interrupt机制挂进去让脚本在交付前停在审核状态。这种设计在真实场景里很实用你不需要让 Agent 一次做到 100% 完美它能做到 80%剩下 20% 交给人在规定节点上把关已经很能节省人力。我强烈建议任何准备上生成式自动化项目的人都在流程图里预留一个人工闸口否则项目上线后的容错率会非常堪忧。3.4 实测一下它生成的脚本长什么样用一条简单的登录用例来测试输入是“打开登录页输入用户名 admin输入密码 123456点击登录断言跳转到首页板标题”系统最终输出的核心代码如下from playwright.sync_api import sync_playwright, expect def test_login(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(http://localhost:3000/login) page.locator(#username).fill(admin) page.locator(#password).fill(123456) page.locator(button[typesubmit]).click() expect(page).to_have_title(首页) browser.close()当然真实测试用例不会这么干净所以我在 Prompt 层面做了几条硬约束必须使用项目现有的选择器约定、必须显式等待关键元素、断言必须覆盖用例给出的验收点。这套约束写进系统提示词后代码风格基本稳定人工审核成本明显下降。4. 常见问题与排查速查4.1 安装与版本兼容实际踩坑中很多问题不是代码逻辑问题而是安装和版本。先说 conda 的选择用 conda 创建独立环境是必须的别直接装到 base 环境。Python 版本优先选 3.11装 LangChain、LangGraph、Chroma 以及 Playwright 插件时兼容性都很好。如果发现某个包安装时卡在编译环节多半是 Python 版本过高conda 换版本重来是最快的办法。常见的错误还有两个一个是langchain_community.llms.Ollama和langchain_ollama.OllamaLLM混用另一个是langchain-openai和langchain_ollama的依赖冲突。我的建议是先在虚拟环境里pip install全量依赖并跑通一个最小 demo再把版本号记录在案再开始写业务逻辑。这个顺序不能省。4.2 RRF 合并检索的“去重缺陷”检索融合时很多项目会采用 RRFReciprocal Rank FusionLangChain 的 EnsembleRetriever 和 Java 生态的 LangChain4j 都有对应的实现。但我发现默认实现存在一个容易忽略的缺陷去重逻辑不完整多个检索器召回同一文档的不同 chunk 时会被当作不同文档重复计算得分导致结果排序被重复片段抬高。这个问题在混合检索BM25 加向量检索场景下尤其明显。比如同一段历史脚本被摘要向量召回了一次、又被原文 chunk 召回了一次RRF 默认会把它算成两个结果两个分相加后排名可能压过了真正匹配但只被召回的文档。修复思路是在融合前按文档 ID 或 metadata 去重如果你用的是 Chroma 自带检索器也可以尽量在写入阶段就统一好source字段再在组合前做聚合。这个坑在官方文档里没有写清楚属于做多了自然能发现的类型。4.3 DeepAgents 做到哪一步了和 Claude 差在哪很多人会拿 LangChain 出的 DeepAgents 和 Claude 等闭源模型的 Agent 能力做对比。我自己的体会是这类自动规划型 Agent 在前半程任务拆解上已经做得不错能够把长任务切成若干带依赖关系的子任务并在失败时自主修正。但在需要深度理解真实 UI 状态、处理模糊需求和跨长上下文的场景里它和成熟的闭源模型确实还有肉眼可见的差距主要体现在复杂上下文保持和工具异常恢复的稳定性上。实践中我发现一个规律差距是可以被工程弥补的。把工具返回格式做得足够干净把任务拆得足够小把校验节点做得足够严格开源模型的最终表现会非常接近闭源模型甚至在某些受限任务上更稳定。所以我的建议是别盲目追求“最强模型”先在流程和工具上打磨性价比更高。4.4 LangChain 真的过时了吗这个话题在开发者社区隔几个月就吵一轮。我的判断是LangChain 没有过时但它的定位变了。早期它靠一揽子抽象吸引了大量用户现在很多人转向 LangGraph、LlamaIndex 或者直接用框架原生能力是因为任务变得更复杂、对可控性的要求更高。LangChain 现在更像是连接各类组件和工具的“胶水层”尤其在企业异构环境里它提供的 Tool 机制、Prompt 管理、向量库适配仍然有很强的实用价值。如果你是为了找工作面试准备我建议把重心放在“理解 Agent 和 Workflow 的设计本质上是什么”而不是死记 API。面试官问到 LangChain 时真正有效的能力是讲清楚一个实际业务需求怎么转化为状态图、检索和工具调用。把这个逻辑讲透比背十个 API 都管用。最后分享一个我自己的心得用 LangChain 这类框架做 Agent最大的风险不是框架不稳定而是开发者对流程不够敬畏。把任务拆碎、把工具返回结构化、把人放进审核链路这三件事做到位项目基本不会出大乱子。如果你也打算跑一个类似的“自动生成测试脚本”的项目建议从最小闭环开始先跑通一条用例的生成与校验再逐步扩大用例集你会看到这套系统的潜力。
返回列表