1. 为什么做这个项目:当“写登录脚本”变成高频重复劳动
先说说这个项目的真实来源。在很长一段时间里,我发现自己不断地在写同一类代码:登录某个系统,保持会话,再去做后续操作。不管是做自动化测试、写爬虫采集公开数据,还是帮内部系统做巡检脚本,第一步永远是登录。麻烦的地方在于,每个系统的登录逻辑都不一样——有的走表单提交,有的要先请求一个token再带参访问,有的需要处理动态加载的验证码,有的则是扫码登录或者短信验证码登录。
写一次两次还能忍受,写多了就变成纯粹的体力活。每次都要花时间分析登录接口、看参数怎么构造、摸清cookie和session的交互方式。遇到验证码还得单独研究怎么识别或绕过。这时候我冒出一个想法:能不能让大模型帮忙干这个活?输入一句“用账号密码登录某系统,保持登录态并输出cookie”,它直接给我生成一个能用的Python脚本。这就是“LangChain+Python实现登录脚本智能生成”这个项目想解决的核心问题。
整个项目用LangChain作为大模型应用框架,Python作为执行语言,核心目标是从用户的自然语言描述中解析出登录场景,推理出合理的技术方案,最终生成可运行、可校验的登录脚本。它适合谁参考?如果你经常写自动化脚本,或者团队里总有人反复问“登录怎么写”,再或者你想把“根据需求自动生成工具脚本”这件事做成一个小工具,这个项目的思路都可以直接借走。
本质上,这个项目不是要做“万能登录生成器”,而是搭建一套“自然语言到可执行代码”的流水线,登录脚本只是第一个落地场景。后面你会发现,摸清楚这条流水线的关键环节之后,把场景换成“生成数据清洗脚本”或者“生成文件批处理脚本”,思路完全不用改。
2. 整体方案设计:从自然语言到可运行代码的完整链路
2.1 核心架构:把“生成代码”拆成四个环节
我最早犯的错误是觉得“这不就是让大模型生成一段代码吗”,直接写一个Prompt丢进去就行。但实际做起来发现,问题很多:大模型生成的代码有时候是用不到的假函数,有时候用的是不存在的库,有时候解析网页的方式完全跑不通。真正能用的方案是把流程拆成四段,各自负责一块:
- 意图解析:从用户描述中提取登录类型、目标系统特征、需要的登录态形式。
- 方案推理:基于意图选择合适的技术栈(requests还是Selenium还是Playwright),设计登录流程步骤。
- 代码生成:按方案逐步生成Python代码,每一步都带注释。
- 校验修正:对生成的代码做静态检查和基础运行验证,发现问题后让模型自行修正。
LangChain在这套架构里起到了两个作用:一是把多个Prompt调用串联成流水线,用LCEL语法或者其他链式写法把“解析—推理—生成—校验”串起来,二是通过结构化输出解析功能(比如PydanticOutputParser)让模型返回的内容稳定可用。
为什么一定要结构化输出?这是我在实际踩坑后得出的结论:如果你直接让模型“给出代码”,它会在代码块之外还输出一大堆解释,甚至有时候解释写在代码里,复制出来根本没法运行。用结构化输出的方式强迫它返回JSON:{"login_type": "...", "tech_stack": "...", "steps": [...], "code": "..."}。这样后续每个环节都能拿到干净的数据。
2.2 技术选型:LangChain的哪些模块被真正用到
LangChain库很大,但这次项目里真正用的模块其实就那么几个。
PromptTemplate负责管理提示词模板,把变量安全地嵌入到模板中。PydanticOutputParser负责声明输出结构,把大模型的返回文本解析成Python对象。这两者组合之后,相当于给大模型加了一层“格式约束”。实测下来,使用结构化输出后,解析失败率从最初的20%以上降到了5%以内——这个数字非常关键,因为做自动化流程时,一旦某个环节解析失败,整条链就断了。
还有一个模块是ChatOpenAI,也就是大模型接口的封装层。这里要说明一下,项目本身不绑定某个具体大模型服务,只要是兼容OpenAI接口格式的模型都可以换上去。实际使用中我发现,代码生成场景下,参数量越大的模型越稳,因为代码生成涉及多重逻辑推理和上下文依赖,模型太小容易生成“看起来对、跑起来错”的代码。
2.3 为什么用“分步生成”而不是“一次性生成”
这是整个项目中最重要的一次方案取舍。对比做过两组实验:一组是让模型一次性输出完整登录代码,另一组是先让模型输出“登录步骤清单”,再根据步骤逐段生成代码。
一次性生成的问题是,当系统登录流程比较复杂时(比如先获取验证码图片,再识别验证码,再携带验证码提交表单),模型很容易把某个环节写错——常见的是验证码识别函数里硬编码了一个错误的图片路径,或者把HTTP请求的data参数和json参数搞混。一次性叫出来的代码,表面上完整,细节上一堆小错误。
分步生成就稳妥得多。先让模型写出一个步骤清单,这一步相当于把复杂问题拆解成模型更擅长处理的小问题。然后针对每一步单独生成代码片段,最后按顺序拼接。这个思路其实跟人类程序员的工作方式是一样的:先想清楚有几个步骤,再一步一步写。实测下来,最终代码的可运行率从30%左右提升到了70%以上。
2.4 需要考虑的登录类型覆盖范围
设计之初,我把常见的登录类型列了一遍,至少要覆盖下面这些:
- 表单登录(账号+密码提交到后端接口)
- 基于Cookie的会话保持登录
- 基于Token的Bearer认证登录
- 带图片验证码的登录
- 短信验证码登录(需要用户手动输入)
- 扫码登录(生成二维码后轮询状态)
每一种登录类型的代码模板差异很大。如果让模型自己“临场发挥”,写出来的代码风格和逻辑会很不稳定。我的做法是把这些通用模板写进Prompt里,作为Few-shot示例的一部分,让模型在这些示例的框架内做修改和组合。实验发现,加入示例模板后,生成代码的稳定性和风格一致性提升明显。
这个设计细节值得展开说。所谓“示例模板”不是让模型照抄,而是给它提供一个“骨架”。比如表单登录的骨架是:构造Session -> 获取必要的隐藏字段 -> 提交表单 -> 校验响应状态 -> 返回Session。模型的作用是根据具体目标系统的情况调整URL、参数名、请求头。有了骨架,生成结果就不会跑偏到“用Scrapy去登录一个纯前端应用”这种离谱方向。
3. 实操环节:一步步搭建登录脚本生成器
3.1 环境准备与依赖版本
项目用到的核心库有五个:langchain、openai(或其他兼容SDK)、pydantic、requests、beautifulsoup4。另外建议安装playwright,因为大量现代网站的登录逻辑依赖JavaScript渲染,纯requests搞不定。
虚拟环境方式安装,Python版本建议3.10以上:
python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install langchain langchain-openai pydantic requests beautifulsoup4 playwright playwright install chromium这里的playwright install chromium很容易踩坑。如果你在国内网络环境下执行,下载浏览器内核可能会失败。解决方式是手动下载浏览器包,放到PLAYWRIGHT_BROWSERS_PATH指向的目录。这一步很多人没做,后面程序卡在playwright调用浏览器找不到文件的位置。
3.2 定义意图解析的结构化输出
先定义意图解析的输出结构,也就是告诉大模型“你返回的结果必须长这样”。这里用Pydantic定义:
from pydantic import BaseModel, Field class LoginIntent(BaseModel): target_site: str = Field(description="目标系统名称/URL特征") login_type: str = Field(description="登录方式:form / cookie / token / captcha / sms / qrcode / 组合") tech_stack: str = Field(description="建议使用的技术栈:requests / selenium / playwright") session_format: str = Field(description="需要输出的登录态形式:cookie / token / session对象") special_requirements: list[str] = Field(description="特殊要求列表,例如验证码处理、代理设置、UA伪装等")定义好这个模型之后,要写对应的提示词模板,明确告诉模型:“你将收到用户对登录任务的描述,请解析出以下JSON字段……不要输出解释。”
实际测试中,Field(description=...)里的描述非常重要。模型的输出质量直接受这些描述影响。描述越具体,输出越准确。比如login_type如果只写“登录方式”,模型可能返回“账户密码登录”这种非标准化描述;写清楚“必须是form/cookie/token/captcha/sms/qrcode中的一种或组合”,模型就能严格按枚举值返回。
3.3 解析器的接入方式
使用LangChain的PydanticOutputParser,把它和PromptTemplate绑定:
from langchain.output_parsers import PydanticOutputParser from langchain.prompts import PromptTemplate from langchain_openai import ChatOpenAI parser = PydanticOutputParser(pydantic_object=LoginIntent) prompt = PromptTemplate( template="请解析用户的登录任务描述,提取结构化信息。\n{format_instructions}\n用户描述:{user_input}\n", input_variables=["user_input"], partial_variables={"format_instructions": parser.get_format_instructions()}, ) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1) chain = prompt | llm | parser result = chain.invoke({"user_input": "帮我写一个登录某系统的脚本,账号密码登录,登录后保存cookie到文件"}) print(result.login_type) # form这里有个关键参数:temperature=0.1。生成代码场景不适合高温度,高温度会让模型发散,输出不稳定。我试过temperature默认的1.0,生成的代码三次有两次不一致,降到0.1之后基本稳定。但也不能设置为0,完全0会让模型过于机械,在需要一些灵活推理时会表现笨拙。
另外,LangChain这里用了管道运算符|,这是LCEL的写法。调用流程很清晰定义:先格式化Prompt,再调用模型,然后解析输出。如果某一步失败,错误信息会指明是解析失败还是模型调用失败,排查起来比较方便。
3.4 代码生成环节的提示词设计
意图解析完成之后,接下来是代码生成。这个环节的提示词设计直接决定最终脚本能不能跑。
我的做法是把提示词分成四块:身份定义、任务目标、技术约束、输出格式。
code_gen_prompt = PromptTemplate( template=""" 你是一位精通Python自动化脚本开发的工程师。请根据以下需求生成登录脚本。 目标系统:{target_site} 登录类型:{login_type} 技术栈:{tech_stack} 登录态要求:{session_format} 特殊要求:{special_requirements} 生成代码时必须遵守: 1. 代码必须完整可运行,包含所有import语句。 2. 必须使用try/except包裹关键网络请求,并打印明确的错误信息。 3. 如果涉及验证码,生成代码时用占位符表示,例如 `captcha_code = input("请输入验证码: ")`。 4. 不允许生成伪造的函数,所有调用的函数必须在代码中定义了完整实现。 5. 最终只输出Python代码,不要输出解释、不要使用markdown代码块标记。 需求描述: {user_input} 生成的代码: """, input_variables=["target_site", "login_type", "tech_stack", "session_format", "special_requirements", "user_input"], )第四个约束“不允许生成伪造的函数”是我加了多次测试后筛选出来的。不加这句话时,模型偶尔会生成一个调用login_parser()的代码,但login_parser函数本体不存在。加上这句话之后,再配合校验环节的检查,基本能堵住这个问题。
3.5 使代码可直接落地的关键细节
拿到模型生成的代码后,不能直接写文件就跑。需要做几层处理:
第一,剥掉外壳。有时候模型仍然会输出markdown代码块标记(python ...),需要正则去掉:
import re def extract_code(text: str) -> str: pattern = r"```(?:python)?\s*(.*?)```" match = re.search(pattern, text, re.DOTALL) if match: return match.group(1).strip() return text.strip()第二,做基础语法检查。用Python自带的compile()函数编译代码,能立刻发现缩进错误、括号不匹配等明显问题。
def validate_code_syntax(code: str) -> bool: try: compile(code, "<generated>", "exec") return True except SyntaxError as e: print(f"语法错误: {e}") return False第三,更近一步的静态检查是检查import的库是否存在。很多时候模型会import一个不存在的库或者写错库名。用一个简单的办法:遍历ast解析出的import节点,挨个尝试import。这一步能拦截掉大概10%的无效代码。
这一步我自己写了一个小函数:用ast模块解析代码,拿到所有import的模块名,然后在当前环境里尝试导入(比新建进程快得多)。如果某个库不存在,就把错误信息反馈给模型,让它修正导入或改用其他方案。这个“检测—反馈—修正”循环是这个项目里提升代码可用率的最大功臣。
第四,检查代码中是否包含“TODO”“pass”“NotImplementedError”这类明显未完成的标记。如果发现,直接判定为不合格,触发重新生成。
3.6 整条链路的代码组织
把意图解析、方案推理、代码生成、校验修正串起来,主流程的伪代码大致长这样:
class LoginScriptGenerator: def __init__(self, llm): self.llm = llm self.intent_parser = build_intent_chain(llm) self.code_chain = build_code_gen_chain(llm) self.validator = CodeValidator() def generate(self, user_input: str) -> GeneratedScript: intent = self.intent_parser.invoke({"user_input": user_input}) attempts = 0 while attempts < 3: code = self.code_chain.invoke({ "target_site": intent.target_site, "login_type": intent.login_type, "tech_stack": intent.tech_stack, "session_format": intent.session_format, "special_requirements": intent.special_requirements, "user_input": user_input, }).text code = extract_code(code) issues = self.validator.check(code) if not issues: return GeneratedScript(intent=intent, code=code, issues=[]) attempts += 1 # 把issues反馈给模型进行修正 correction_prompt = CorrectionPrompt(...) code = self.llm.invoke(correction_prompt).content return GeneratedScript(intent=intent, code=code, issues=issues)注意这里的while循环最多重试三次。这背后有一个实测结果:如果三次修正仍然无法通过校验,大概率是这段需求本身描述不够清晰,或者需要的技术超出了当前模型能力。继续重试只会浪费时间,不如把问题抛回给用户去补充信息。
3.7 引入真实的交互体验
对用户来说,使用方式最好简单直接。我在项目里加了一个命令行交互入口:
$ python main.py 请输入你的需求描述: 登录某网站,账号密码登录,登录后把cookie保存到cookies.json,后续请求要用这个cookie 正在解析需求... [解析结果] 登录类型=form, 技术栈=requests, 登录态=cookie 正在生成脚本... [生成结果] 已生成 login_script.py,通过了语法检查和依赖检查 是否尝试运行?(需要该网站可达)(y/n): n这个交互环节看起来不起眼,但极大地提升了项目的实用感。快到验证阶段时,用户只需要输入一句话,几十秒后就能得到一个能运行的、带依赖检查和异常提示的脚本。这里的核心体验是,你把用户从“我要自己分析怎么登录”这个任务中解放出来了。
4. 踩坑实录:几个最影响生成质量的细节
4.1 模型输出中的“虚假模块”问题
这是最让我头疼的问题,没有之一。模型会生成看起来完全合理但实际不存在的Python模块。比如它可能用到一个叫做httpx_auth的库,但这个库并未安装,甚至它声称某个函数来自requests_html(这个库确实存在,但经常被误用)。校验环节用ast遍历import后尝试导入,可以拦下一大部分。
举一个实际经历:有一次测试,模型生成了一段代码,import了fake_useragent,这看起来是一个处理UA的常见库,但它也正确注册了一个自定义UA生成逻辑。问题是这个库本身依赖环境里没有的pyyaml,导致隐式依赖缺失。后来我在校验环节不仅检查了import语句,还检查了import库的间接依赖是否完整。做法很土,就是在一个干净虚拟环境里跑pip install,如果失败就告诉模型换方案。实测下来,这类生成结果的修复率很高。
4.2 验证码处理的策略
登录脚本里最复杂的部分是验证码。无验证码时,登录逻辑相对直白。一旦涉及验证码,模型的输出经常不稳定。常见的问题是生成代码里调用了一个不存在的OCR库,或者把验证码图片URL写成一个近似错误的拼接。
我在设计时定了两条原则:一是默认情况下不要自动识别验证码,而是生成一个交互式输入,让脚本在运行过程中暂停,由执行者去系统里查看验证码再输入;二是如果目标系统确实有开放的验证码识别服务,生成时以占位符方式预留接口。第二条原则相当于告诉模型,“你不必真的去解决验证码识别问题,你只需要在这里留一个明显的接口位置”。
这背后有一个教训:不要试图让语言模型“幻想”出验证码识别能力,它不具备图像处理和验证码推理的实质能力(除非它内部集成了相应工具)。在架构上明确边界,比构造一个看似聪明但实际不可行的流程聪明得多。
4.3 动态网页与JavaScript渲染
很多“登录系统”不是传统的表单提交,而是前端渲染的动态页面。用户描述可能只是“打开某网站,点击登录按钮,输入账号密码,点击提交”。如果技术栈误选了requests,后面会撞上一堆问题——因为requests无法执行JavaScript。
这个项目里我处理的方式是,在意图解析阶段,额外要求模型判断目标系统是否需要JavaScript渲染。判断依据是用户描述里有没有“打开页面”“点击”这类交互性词汇。但更稳妥的做法是,在生成完代码后,让用户看到技术栈选型,并提供一个手动切换选项:如果用户反馈“这个网站是单页应用”,就强制切换到Playwright技术栈重新生成。
一开始我没有这个手动选项,失败率特别高。后来我专门加了一个参数force_engine,允许用户指定必须使用Playwright或者必须使用requests。这个开关看似简单,实际把复杂场景救了回来,增加最终成功率20个百分点以上。
4.4 长代码生成时的上下文截断问题
验证码系统描述比较复杂时(比如“登录后还要经过一次MFA校验”),一次生成长代码很容易出现上下文截断或者逻辑断裂。我原来的链是直接输入整段需求,让模型从头生成到尾。后来改成“分步生成、逐步拼接”,把需求拆成“获取验证码图片→识别或输入验证码→提交登录→保存会话”四段,每一段单独生成代码再拼接。
拼接不是简单字符串相加。分步生成时,后一步会依赖前一步的某个变量名,比如session、csrf_token。如果不加约束,两步生成的变量名可能不一致。我的处理方式是,中间加一个“整合”环节:把各段代码交给模型一次,让它统一变量名,形成最终脚本。虽然多了一次模型调用,但产出的代码一致性提升非常多。
4.5 模型对目标URL的“幻觉”
有一个特别隐蔽的问题:当用户描述一个知名网站时,模型会“自由发挥”它记忆里的URL或接口路径,但这个URL往往早已失效或者根本是不完整的。比如,有一段描述是“登录一个内部的运维平台”,模型生成的代码里URL字段居然是http://10.0.0.1/login,显然不真实。
我后来在意图解析结构里加了一个字段url_unclear,并在流程中加入一个二次确认机制:如果模型认为目标URL不确定,就会在生成的代码里写BASE_URL = input("请输入目标系统URL: "),运行时让用户自己填。这个改动虽然降低了“自动化程度”,但大幅提升了脚本的可用性。反过来想,大模型再强也并没有真实访问目标系统的能力,它只是参考记忆来拼装,所以在关键URI上保持开放性输入才是稳妥的选择。
5. 常见问题排查速查与安全边界
到这里,我把使用过程中经常撞到的问题整理成表格,方便直接查阅。这些问题的排查思路都经过实践检验,尤其是表中前三条,基本覆盖了八成以上的失败场景。
| 现象 | 首次快速检查 | 更根本的解法 |
|---|---|---|
| 生成的代码有import的模块找不到 | 检查环境是否安装该库 | 在校验环节加入AST import检查并回传模型修正 |
| 代码可以编译但运行时报连接错误 | 确认目标网站可达、URL是否写死 | 改为运行时input()输入URL,或启动交互确认机制 |
| 验证码步骤无法实施 | 查看是否是OCR库不存在或识别效果差 | 改为交互式手动输入验证码,不要在生成层强行解决识别 |
| 技术栈选型错误(该用Playwright用了requests) | 确认目标是否前端渲染 | 增加force_engine参数,允许用户指定技术栈 |
| 多次重试仍失败 | 确认需求描述是否完整(缺少URL?缺少登录方式?) | 将缺失信息问题的反馈原样展示给用户,发起一次澄清问答 |
| 模型返回包含Markdown代码块导致解析失败 | 检查提取代码的正则逻辑 | 在提示词中明确“不要用代码块标记”,并在解析阶段做剥离 |
| 长代码生成中途逻辑断裂 | 查看是否有半截函数、未闭合的定义 | 改成分段生成后拼接,最后加整合统一变量名 |
这里有一点不得不强调:这个项目生成出的脚本,一定要在受控环境里先测试,不要直接拿着它去登录一个生产系统。一方面,生成脚本的对象是“自动化操作”,如果对登录接口的调用频率过高,可能给目标系统造成不必要的压力;另一方面,登录态本身可能涉及账号安全性,随便用生成脚本去跑别人的系统,很可能触发风控,弄不好还会把自己的账号弄封。我的习惯是,生成完先本地review,再看那个目标系统允不允许自动化操作,最后才在测试环境执行。
安全边界这块还有一个容易被忽视的细节:不要把真实的账号密码直接硬编码进生成的脚本。在提示词里我也会约束模型,密码字段必须用os.getenv("LOGIN_PASSWORD")这种方式来读取,或者用getpass让运行时输入。这是基本的安全习惯,但大模型默认不会主动这么写,需要你明确在Prompt里塞进去。我在第一批实测脚本时就踩了一次坑——生成的代码里直接明文写密码,赶紧改掉了。
6. 代码结构里藏着的几个工程细节
6.1 如何组织保存生成的脚本
生成的代码不应该只是简单落盘。我在项目中加入了一个元信息头,自动写在生成脚本的顶部注释里:
# 自动生成时间: 2025-xx-xx # 生成参数: login_type=form, tech_stack=requests # 说明: 该脚本由语言模型生成,请在测试环境验证后再使用不要小看这几行注释。没有元信息时,拿到脚本的人会以为这是一个手工写的脚本,发现问题会去逐行debug,这浪费了大量精力。标注清楚来源后,后续维护时能很快判断问题方向。
6.2 依赖记录与运行隔离
生成的脚本如果需要额外的依赖(比如某个特定版本的库),我会生成一个requirements.txt片段,放在脚本同目录。这样使用者不需要手动去“猜”缺哪个库。更稳妥的做法是有一个scripts/venv/目录,每次生成的脚本都在独立的虚拟环境中运行,完全隔离依赖。
我一开始觉得虚拟环境是多余动作,但后来自己又生成了一段时间的脚本,发现如果不隔离,早晚会有一个库版本冲突出来制造惊喜。隔离之后,生成脚本的可复现性好了非常多。
6.3 多轮对话能力
最初的版本只支持单次描述、单次生成。后来我发现,用户往往需要和“生成器”对话才能说清楚需求:“我要登录某网站……嗯,那个网站要先滑动验证……对,是滑块验证码,不是图片验证码……另外登录后还要跳转一个二次页面确认。”这种信息是一步步挤牙膏挤出来的。
基于这个观察,我在系统中加入了简单的多轮对话继承功能:把历史消息塞入上下文,让模型在其中识别“新增”和“变更”的信息。这不算复杂,但实在提升体验。如果你在复现这个项目,我建议在意图解析阶段就把历史摘要包含进去——不要只依赖用户最后一次输入。
7. 个人体会与可扩展方向
做了这个项目之后,我最大的体会是:LangChain在这里不是一个“妙不可言”的框架,而是一个把“模型调用—提示词管理—输出解析—流程控制”组织起来的实用工具。它真正的价值不是封装了一个多厉害的AI接口,而是逼着你把任务拆成有边界的模块,并让每一步的输出都有结构和逻辑可校验。与其说这是在做AI应用,不如说是在做工程化。
登录脚本智能生成的链路一旦跑通,往旁边扩展的想象力就打开了。我在项目结束前做了几个实验:换掉登录场景,改成了“生成文件批处理脚本”和“生成自动化测试报告脚本”,完全复用同一套架构,只替换了提示词模板和输出结构定义。换场景的成本低到出奇,几乎就是换一个意图解析的Schema再加一批示例的事。
一句话总结:这个项目里头真正值钱的不是登录脚本本身,而是那条“自然语言描述—结构化意图—分步代码生成—静态校验—迭代修正”的管道。你随时可以把管道出口的东西换掉,管道本身成了复用资产。
有一点我必须再强调一遍:校验环节是整条管道的灵魂。没有校验,大模型会给你一个看上去很美、实际上跑不起来的“幻觉代码”。有了校验,它才真正变成了一个工程环节和可靠工具。每次上报“校验不通过”的附加信息,都会让模型在下一次生成时更加谨慎。这也是我建议后来者最先着手优化的部分——不要急着换更大的模型,先把这层“质检网”织牢。