news 2026/9/26 18:41:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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、管理向量库、定义 Tool;LangGraph 则是一个流程编排框架,它把任务状态显式建模成一张图,支持条件分支、循环重试、人工介入。

打个比方: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 python=3.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 设为 512,chunk_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_name="summary", embedding_function=embeddings) docstore = InMemoryStore() retriever = MultiVectorRetriever( vectorstore=vectorstore, docstore=docstore, id_key="doc_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, k=3) 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(cases=state["test_case"], examples=state["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(headless=True) page = browser.new_page() page.goto("http://localhost:3000/login") page.locator("#username").fill("admin") page.locator("#password").fill("123456") page.locator("button[type='submit']").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 合并检索的“去重缺陷”

检索融合时,很多项目会采用 RRF(Reciprocal Rank Fusion),LangChain 的 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,最大的风险不是框架不稳定,而是开发者对流程不够敬畏。把任务拆碎、把工具返回结构化、把人放进审核链路,这三件事做到位,项目基本不会出大乱子。如果你也打算跑一个类似的“自动生成测试脚本”的项目,建议从最小闭环开始:先跑通一条用例的生成与校验,再逐步扩大用例集,你会看到这套系统的潜力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 18:40:12

二重积分积分限怎么定?画图+穿线法全流程拆解

拿到二重积分的题目&#xff0c;很多同学第一反应是背公式&#xff1a;直角坐标怎么写、极坐标怎么写、先对谁积分、后对谁积分。可一到做题就露馅&#xff0c;尤其是给一个具体的积分区域&#xff0c;比如由抛物线和直线围出来的那种&#xff0c;完全不知道上下限该从哪里抄&a…

作者头像 李华
网站建设 2026/9/26 18:39:03

Adobe全家桶绿色优化版部署指南:从环境配置到故障排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 18:38:48

沙箱托管Agent Harness:OpenAI Agents API 接入与运维实践

最近大模型圈的 agent 热潮算是真正走到工程阶段了&#xff0c;OpenAI Agents API 出来后&#xff0c;写一个带工具调用的 agent 不再是什么难事——难的是把它稳定地跑成一个服务。你要解决算力从哪来、环境怎么隔离、多实例怎么调度&#xff0c;还要处理那个比 agent 本身更容…

作者头像 李华
网站建设 2026/9/26 18:37:49

电力系统潮流计算:牛顿-拉夫逊法与P-Q分解法的MATLAB实现

潮流计算在电力系统里属于那种“看起来简单、写起来全是细节”的东西。很多教材把公式推导梳理得很漂亮&#xff0c;但一到 MATLAB 里自己动手&#xff0c;就会遇到雅可比矩阵符号搞混、迭代发散、P-Q 分解法在某个算例里死活不收的尴尬。我当初就是因为不满足于直接调工具箱&a…

作者头像 李华
网站建设 2026/9/26 18:37:06

Java GC核心知识点全梳理:GC Root、循环引用与三色标记法详解

Java GC核心知识点整合&#xff1a;从GC Root到三色标记法的一次彻底梳理每次排查线上OOM或者JVM频繁Full GC的时候&#xff0c;我总会习惯性地先打开堆转储文件&#xff0c;顺着引用链一路往上翻。翻到最顶端的某个"根"时&#xff0c;真相往往就藏在那条引用链上。这…

作者头像 李华
网站建设 2026/9/26 18:36:56

电动汽车V2G技术:从削峰填谷到虚拟电厂的实操指南

傍晚六点&#xff0c;城市用电曲线跟过山车一样往上冲&#xff0c;发电厂拼命加负荷&#xff1b;凌晨两点&#xff0c;大家睡了&#xff0c;用电需求掉到谷底&#xff0c;发电厂又得压着功率跑。这种“白天抢电用、晚上电没用”的错位&#xff0c;正是电网最头疼的问题。现在来…

作者头像 李华