☰
用LLM+浏览器自动化打造GitHub智能代理:Clawless实战解析
2026/10/20 5:06:21 网站建设 项目流程

1. 项目背景与需求拆解

1.1 为什么我给 GitHub 操作装了一个“智能代理脑”

做开发这些年,GitHub 上的日常操作其实非常规整:看 issue、关 issue、拉分支、提交 PR、检查 CI 状态、回复评论。这些操作本身不难,难的是“一天要重复几十次”。尤其是维护几个活跃仓库的时候,很多时间都耗在点按钮、复制链接、粘贴模板上面。我一直在想要不要写一套自动化脚本,可是脚本这东西有个毛病——写死了。今天想按仓库名搜 issue,明天想按标签过滤,后天可能又要导出某段时间所有 PR 的评论。每换一个需求就要改代码,维护成本比手动操作还高。

后来我意识到,真正需要的不是一套固定流程,而是一个“听得懂人话”的中间层。用户用自然语言说一句话,比如“把 A 仓库最近两天没人回复的 issue 列出来,顺便生成一份摘要”,系统能理解这个意图,自己决定要打开哪些页面、点哪些按钮、读取哪些数据,最后把结果汇总成报告。这就是 Clawless 的核心定位——它不是又一个 GitHub API 封装库,而是一个基于 LLM 与浏览器自动化的智能代理。

1.2 为什么不直接用 GitHub API 搞定一切

很多人的第一反应可能是:GitHub 官方 API 那么全,为什么还要让浏览器自动化去点页面?这里有几个实际原因。

第一个原因是“未登录态和权限边界”。GitHub API 确实很完善,但在某些场景下,比如你想看某个组织私有仓库的报表,或者模拟用户在网页端的操作,API 的权限模型和网页端并不完全一致。用浏览器自动化相当于“所见即所得”,你能在页面上做到什么,代理就能做什么,不需要额外申请一堆 token。

第二个原因是“页面本身就是最高效的信息容器”。很多数据在 API 里要多次调用才能拼出来,但在网页端一次渲染就全部展示了。比如某个 issue 的整个讨论串,包括相关人员、标签、里程碑、引用关系,API 要拆分好几个端点才能拉全,而浏览器直接抓取渲染后的 DOM 就能一次性拿到整块内容。

第三个原因更实际——我们想让 LLM 的输出直接作用于真实界面。LLM 擅长理解复杂的上下文和模糊指令,浏览器自动化擅长执行确定性的 UI 操作。两者结合,LLM 负责“思考下一步做什么”,浏览器自动化负责“把思考变成点击动作”。如果只走 API,那 LLM 的很多优势体现不出来,因为 API 调用本身是结构化的,少了一个“让模型自己决定怎么查”的中间层。

这不是说 API 没用。Clawless 里实际上也用了 GitHub API,用于获取稳定的结构化数据,比如仓库元信息、commit 状态。而浏览器自动化主要处理那些需要“看懂页面”的场景。两套手段互补,才是这个项目的核心思路。

2. 项目整体设计与架构解析

2.1 模块划分:LLM、浏览器控制、任务队列三件套

Clawless 的整体架构并不复杂,核心可以拆成三个模块:意图理解模块、浏览器自动化执行模块、结果整理与反馈模块。意图理解模块负责接收自然语言指令,通过大模型将指令解析成一系列可执行的操作步骤;浏览器自动化执行模块负责驱动真实浏览器环境,在 GitHub 页面上完成这些步骤;结果整理模块负责把中间抓取到的页面内容喂回给大模型,生成最终的用户可读汇报。

这三个模块并非简单的流水线,而是一个带反馈的循环。比如用户说“看一下这个 PR 有没有冲突”,代理会先打开 PR 页面,观察是否有“This branch has conflicts”的提示,如果有,再进一步查看冲突涉及哪些文件。这个过程需要多次“观察-决策-行动”循环,而不是一次性把所有步骤规划完。

我把这种执行方式称为“半自主循环”。为什么是半自主?因为在大模型给出操作序列之前,我会先定义一组可用的原语操作,比如 open_repo、click_issue、extract_text、scroll_page 等等。大模型不能自由发挥去执行任意 JavaScript,只能在原语集合里面选。这样既保留了灵活性,又确保了安全性。毕竟浏览器自动化一旦失控,可比 API 调用失控危险多了。

2.2 关键技术选型:Playwright 与 LLM 的配合

浏览器自动化这一块,我选择的是 Playwright。原因很直接:它对现代浏览器的控制能力足够强,支持页面快照、网络拦截、iframe 处理、文件下载,而且自带多浏览器支持。相比老牌的自动化工具,Playwright 的等待策略和对单页应用的友好程度要高很多。GitHub 的页面是典型的动态渲染,大量的内容是异步加载的,用传统工具很容易踩“元素找不到”的坑,Playwright 的自动等待机制能省下不少事。

LLM 部分,我采用的是本地运行的开源量化模型加云端 API 混合策略。为什么会有这种混合?因为意图识别对模型能力要求比较高,尤其是理解“把 A 仓库最近两天没人回复的 issue 列出来”这种带有时间窗口、过滤条件和输出格式约束的句子,小模型容易翻车。所以在关键决策节点,我会调用能力更强的云端模型;而在一些简单的文本分类、字段提取环节,用本地小模型就足够了,速度更快也省成本。

整个系统通过一个事件总线来通信。LLM 输出的决策被包装成结构化指令,浏览器自动化模块执行完以后,把观察到的页面摘要发回到总线上,再由 LLM 决定下一步动作。这有点像一个带眼睛和手的 Agent,眼睛是 DOM 快照,手是自动化操作。

2.3 架构上的几个取舍与避坑思路

第一版设计时,我曾想过完全让 LLM 写 JavaScript 代码来操作页面,后来发现不可行。原因是生成的代码错误率太高,而且页面一旦改版,生成的代码就失效了。改成“原语函数 + 参数化调用”之后,模型只需要输出函数名和参数,可靠性提升非常明显。

另一个取舍是同步执行还是异步执行。早期的想法是每次指令都立刻执行,但实际用下来发现,很多任务执行时间较长,比如批量导出几十个 issue,中间还可能遇到页面加载慢、验证码等问题。现在我改成了任务队列模式:用户提交指令后,任务先排队,代理逐个执行,并通过日志实时推送进度。这样即使某一步卡住了,也不会阻塞整个会话。

架构里还有一个不可忽略的模块:安全性控制。默认情况下,所有读操作(查看 issue、搜索代码、读取评论)都是允许的,但写操作(创建 issue、关闭 PR、合并分支)必须经过二次确认。这个二次确认不是简单的弹窗,而是让 LLM 先把“即将执行的动作”翻译成自然语言描述,用户确认后才执行。从实际体验看,这个设计阻止了大量误操作。

3. 核心实现细节与关键步骤

3.1 环境搭建与依赖准备

Clawless 使用 Python 作为主语言,原因在于 LLM 生态和数据处理库都集中在 Python 上,和 Playwright 的 Python 版本配合也没什么障碍。整个项目环境建议使用 Python 3.10 以上版本,因为后面要用的某些类型注解特性在旧版本上解析会出问题。

pip install playwright playwright install

这两条命令做完后,再用自带工具安装浏览器内核。如果是在服务器上跑,还要额外装系统依赖。这一步最容易踩坑的是 Linux 环境缺库,会导致浏览器启动时崩溃。解决办法是执行 Playwright 官方提供的系统依赖安装脚本,或者根据报错补装对应的共享库。

除了 Playwright,还需要安装大模型相关的库。如果使用云端 API,直接装官方 SDK 即可;如果要在本地跑开源模型,需要额外部署推理服务。我在本地测试时用的是量化到 4bit 的小参数模型,占用的显存大约 6GB,日常任务足够用,但遇到稍微复杂的指令就会有点吃力。后来把核心推理切换到云端模型,本地模型只用来做字段抽取,整个系统的稳定性就上来了。

环境配置中最容易忽略的是“并发控制”。浏览器自动化是重资源操作,同时开太多页面容易把内存吃满。我在代码里加入了一个简单的信号量,保证同一时间最多只有三个浏览器上下文在运行。如果任务队列里同时进来十个任务,多出来的会排队等待。

3.2 让 LLM 将自然语言指令转换成可执行计划

这一节是整个项目的灵魂,也是最难做好的部分。大模型本身并不会用浏览器,我们需要教它“以什么样的格式输出决策”。

我的做法是定义一套严格的工具调用协议。LLM 每次输出都会包含一个 JSON 对象,字段说明如下:

{ "thought": "简要说明当前观察到的情况和决策理由", "action": "要调用的原语函数名", "parameters": { "arg1": "value1" } }

这个输出会被解析后交由执行器处理。为了确保模型不会输出格式之外的东西,我在 prompt 里放了一个成功案例和两个失败案例。少样本提示的效果比单纯说“请以 JSON 格式输出”要好得多,因为模型天生擅长模仿模式。

任务拆分是另一个关键点。用户一句“帮我看看这个仓库的 health 情况”,我们不可能靠一次浏览器动作拿到所有信息。我们会先把任务拆成几个子步骤:打开仓库主页、读取 README、查看最近 issue 和 PR 数量、检查 CI 状态,最后汇总。拆分子任务既可以在模型内部完成,也可以通过一个独立的“规划器”来做。我选择让规划器调用一个固定的工具函数来生成步骤列表,这个列表会被展示给用户确认,用户只确认“大方向对”就行,细节由代理自行调整。

实际跑下来会发现,LLM 在规划阶段容易出现“过度设计”。比如用户只想看一个 issue,模型偏要打开仓库首页再点进去。这里可以用一个简单策略:prompt 里明确要求“以最少步骤完成目标”。有了这条约束,模型的规划会明显精简。

3.3 浏览器自动化操作 GitHub 页面的实现细节

Playwright 操作 GitHub 页面的语法并不复杂,难的是等待条件。GitHub 很多元素是异步渲染的,加载时间也不固定,如果直接用 click 操作,经常会碰到元素还未挂载的情况。我封装了一个 wait_for_selector 的函数,并且结合文本匹配来定位元素。

下面是一个典型的打开 issue 页并抓标题的实现片段:

async def open_issue(page, issue_number): await page.goto(f"https://github.com/some/repo/issues/{issue_number}") await page.wait_for_selector("h1.gh-header-title", timeout=10000) title = await page.text_content("h1.gh-header-title") comments = await page.query_selector_all(".timeline-comment") return {"title": title.strip(), "comment_count": len(comments)}

这段代码用h1.gh-header-title定位标题,用类名.timeline-comment定位评论块。这里有一个细节:GitHub 的类名偶尔会调整,所以不能写死一个选择器。更稳妥的做法是同时启用多个备选选择器,例如先按类名找,找不到再按 aria-label 找。浏览器自动化的核心思想是“不是把某个按钮点出来,而是把事情办成”。

对于需要登录的场景,我更推荐直接注入 cookie 而不是每次都走登录流程。把 GitHub 的登录态 cookie 存到本地,启动浏览器时通过context.add_cookies()添加进去,这样页面会直接识别为已登录状态,避免每次执行任务前都要手动扫码或者输密码。当然,cookie 有有效期,过期后会自动跳转到登录页。针对这种情况,我在执行循环里加入了一个“登录检测”步骤:如果页面出现登录表单,就暂停任务,通过邮件或者钉钉通知人工介入。

3.4 代理策略:让 LLM 决策与页面状态保持同步

很多人做 LLM 加自动化的 demo 时,会让 LLM 生成“一串”操作,然后一次性执行。这在简单固定流程中没问题,但在真实页面上会很脆弱。GitHub 的页面状态经常跟随用户操作发生变化,比如点击一个标签后可能展开新的导航菜单,滚动加载后才会出现更多评论。如果 LLM 的决策不是基于“当前页面”的状态,那所有后续操作都会建立在错误的前提上。

Clawless 采用的方法是“一次只走一步”。每一步开始前,先从当前页面提取一个精简的状态摘要,包括页面标题、当前 URL、可见的主要按钮文字、issue 或 PR 的标题列表。这个摘要通过一个上下文压缩函数处理,去掉大量无关的导航和 DOM 噪音,保留决策所需的信号。然后把这个摘要发给 LLM,让它基于摘要输出下一次动作。

这里有个值得分享的经验:摘要不是越长越好。一开始我试图把整个页面的 body 文本都塞给模型,结果不仅 token 消耗大,模型还容易被 sidebar 里的无关链接带偏。后来我改成只提取“可交互元素”和“目标内容区域”的文本。比如在 issue 列表页,只提取每个 issue 的标题、编号、标签和更新时间;在 issue 详情页,只提取正文、评论者、状态、时间线核心事件。这样模型的注意力集中,准确率也高了。

如果 LLM 输出的动作与页面实际可执行的操作不匹配(比如点击了一个不存在的按钮),执行器会返回一个“操作失败”标志,并附上失败原因。这个原因会回传给 LLM,让它重新判断。这种带反馈的循环是保证可靠性的关键,也是 Clawless 和普通脚本自动化最大的不同。

4. 常见问题与排查技巧实录

4.1 页面元素定位失败频繁,怎么处理

用浏览器自动化做 GitHub 操作,遇到最多的报错就是超时和元素找不见。GitHub 页面之所以难定位,是因为其 DOM 结构在不同功能模块间非常不一致,而且经常通过 A/B 测试调整样式。我刚开始写抓取脚本时,经常今天能跑通的代码明天就挂了。

排查思路第一是看选择器是否太严格。比如我一度用button[data-testid="new-issue-button"]来定位新建 issue 按钮,但这个属性只在部分页面存在。后来改成文本匹配优先、属性匹配兜底的策略,先用get_by_text("New issue"),实在不行再查button元素里的文本。Playwright 自带的get_by_role和get_by_text比裸query_selector要稳很多,因为是语义化定位。

第二是处理懒加载。GitHub 的评论和通知是分段加载的,滚动到页面底部才会加载新内容。对于“导出所有评论”这种需求,不能只抓一次就完事,而应该循环执行滚动,直到页面高度不再变化,然后判断是否到底。我封装了一个scroll_until_end的函数,每次滚动后等待 500 毫秒,再比较新的滚动高度与旧值是否相同,如果相同就说明已经到达底部。

第三种常见情况是页面弹窗和悬浮层遮挡。GitHub 的导航栏有固定定位,有时会挡住左侧的元素,导致 Playwright 点击时提示元素不可见。解决办法是点击前先执行element.scroll_into_view_if_needed(),然后再点击。这个操作大部分时间有效,如果还是失败,就换成 JavaScript 直接触发点击事件:page.evaluate("(el) => el.click()", element)。

4.2 LLM 指令理解偏差和优化技巧

Clawless 使用 LLM 的最大痛点不在于模型能力,而在于“提示词的表达能力”。有时候用户说“最近没人管的 issue”是什么意思?没人回复超过 48 小时?没有任何 label?还是 assignee 为空?这种语义歧义如果放到真实场景中,会导致代理的理解和用户预期相悖。

我的解决方式是定义“领域术语扩展表”。在把用户指令发给模型之前,先经过一个预处理器,把常见的模糊说法映射为具体的筛选逻辑。比如“没人管”统一映射为“没有 assignee 且最近 7 天没有评论”,“没动静”映射为“30 天内没有活动”。这个映射规则可以放在配置文件里,用户也能自己调整。预处理器本质上是一个文本分类模型,用本地小模型就可以跑。

另一个技巧是让 LLM 输出时附带上“假设”。例如它计划点击某个按钮前,先描述“我准备点击‘Close issue’按钮,因为该 issue 的用户已确认问题解决”。如果这个假设和实际页面不符,执行器可以拦截并纠正。虽然多了一步验证,但安全性提升很大,尤其是涉及写操作的场景。

如果发现 LLM 反复生成无效的重复动作,比如不断尝试点击同一个已失效的按钮,我会在 prompt 中加入“历史动作列表”,让模型知道哪些操作已经尝试过且失败了。这相当于给模型一个短期记忆,避免原地打转。循环超过三次之后,代理会主动停止并询问用户是否继续,防止死循环消耗资源。

4.3 浏览器环境下的身份验证与安全边界

GitHub 有时候会要求二次验证,比如输入验证码或者确认设备。这类验证无法通过自动化代码直接绕过。我的做法是在执行流程中插入“人工辅助点”:

  • 当检测到需要验证码时,代理发消息给控制端,附上当前页面的截图;
  • 控制端人员查看截图后手动输入验证码;
  • 代理继续执行后续任务。

这个流程听起来笨重,但实际用下来效率很高,因为验证码不是每次都有。需要设置超时时间,比如 5 分钟没有人工响应就放弃该任务,进入队列尾并标记为需要人工处理。

安全边界方面,我在执行器中单独维护了一个“受限操作列表”。凡是在列表内的操作,比如 force push、删除分支、修改仓库设置,一律拒绝执行。即使 LLM 明确输出了对应动作,执行器也会返回“operation not allowed”。这个保护机制不能写在 prompt 里,因为 prompt 可以被绕过,必须写在代码层,作为绝对的规则。

最后还要提一下 cookie 泄露的问题。所有自动化相关的凭证文件都不能提交到公开仓库里,建议使用环境变量或者专用配置目录,并且设置.gitignore。我在某个模拟项目里就见过有人把 cookie 文件传上去的,还好及时发现,不然账号就危险了。

5. 实际运行效果与经验总结

Clawless 完整跑通后,我把几个高频场景放进去做了测试:自动整理每周 issue 周报、批量筛选无人认领的 bug、生成 PR 变更摘要并发送到聊天工具。效果比预期好不少,尤其是在“生成摘要”这个环节,LLM 可以把页面上的零散信息整理成带逻辑顺序的短文,省掉大量人工整理时间。

但也有几个地方没有想象中完美。首先是执行速度。浏览器自动化比 API 调用慢得多,一个简单的任务通常需要 10 到 30 秒,复杂的甚至要几分钟。如果用户想要非常快速的反馈,这套方案就不太合适。所以后来我在架构里加了一个“快速任务”分流——需要秒级响应的操作直接调 API,只有理解成本高的任务才走 LLM 加浏览器这条路径。

另一个是 token 开销。每次抓取页面摘要后发给 LLM,长途请求的 token 消耗不小。为了控制成本,我设置了一个页面摘要的压缩比例,很多无关文本会被截断。如果摘要超过 4000 字符,就只保留前 1500 字符和最后 800 字符,因为关键信息通常集中在开头和结尾。

我在实际使用中最深的一个体会是:这类项目真正难的不是模型怎么选,也不是自动化框架怎么用,而是“信任边界”怎么设计。LLM 让系统看起来有了智慧,但智慧的另一面是难以预测。只有把决策的范围限制在明确的原语函数集里,把高风险操作挡在代码层,才敢放心让代理去操作真实仓库。Clawless 走到今天,最值钱的不是那几段代码,而是这套边界设计。

如果你也想做类似的东西,我建议从一个非常窄的场景切入,比如“自动整理某个 issue 列表并生成报告”,不要一上来就想着覆盖所有 GitHub 操作。先把一条链路跑顺,再扩展其他能力。同时建议尽早接入日志系统,把所有 LLM 决策和浏览器动作记录下来,这样出了问题可以回溯,也方便调优提示词。给代理加一条“日志就是安全绳”的原则,能省掉很多后期排查的麻烦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询