像很多老读者一样,我平时对付"没有文档的API"这件事,大多数时候靠着DevTools的Network面板加上自己的观察力。拿到一个请求URL,分析参数,复制响应JSON,然后对着类型定义一个个手敲——运气好的时候接口只有两三个,运气不好的时候一个页面里几十个调用,光梳理就够受。所以当我开始认真琢磨"能不能让LLM先替我做一遍逆向、再把结果直接生成生产可用的客户端代码"时,我发现这条链路虽然想法简单,实际执行起来却有一堆细节要打磨。
这篇内容要讲的,就是一套用LLM配合浏览器自动化,自动捕获API流量、推断接口语义、最终生成生产级API客户端的完整方案。不是PPT思路,而是我实际跑通的流程:如何拦截浏览器请求,如何用提示词让LLM输出稳定的schema,如何让生成的客户端真正能编译、能跑通、能处理重试和错误。适合正在做API兼容层、爬虫框架、内部工具链,或者单纯想把人从重复劳动里解放出来的开发者和架构师参考。
整条链路分四层。第一层是浏览器自动化,解决"API从哪里来";第二层是流量清洗与聚合,解决"哪些请求值得分析";第三层是LLM分析与代码生成,解决"怎么把观察变成代码";第四层是自动化验证,解决"生成出来的东西能不能用"。后面我会沿着这条主线,把这些环节逐一拆开,穿插我自己踩过的坑和调整过的设计决策。
1. 方案选型:为什么是LLM加浏览器自动化,而不是传统抓包直接写代码
1.1 传统API逆向的痛点到底出在哪
先说说在没有这套方案之前,手动逆向一条API链路最花时间的地方在哪里。表面上是在"看请求",但实际工作量集中在四块。第一,要找到哪个请求对应哪个业务动作,一个页面上可能同时有几十个请求在跑,得按时间、参数、返回内容去猜。第二,要理解参数和返回字段的语义,很多接口字段名是缩写或混淆过的,不能靠肉眼直接看懂。第三,要处理动态内容,token、时间戳、签名参数每次都会变,直接复用会失效。第四,要把观察结果写成代码,光是定义几十个接口的请求响应类型,就够写一上午。
这四块恰好都是LLM擅长的。LLM能把"埋在一堆网络流量里的模式"提取出来,能根据上下文语义推断字段含义,也能直接产出结构清晰的类型定义和调用代码。而浏览器自动化的价值在于,它能替我们模拟真实用户操作,把页面交互过程中产生的请求完整捕获下来。两者的结合点很自然:浏览器自动化负责提供"原材料",LLM负责把原材料加工成"产品"。
1.2 为什么选Playwright而不是纯网络层抓包
我之前也试过在系统层抓包,比如用网络代理工具截HTTP流量,或者用系统级的方式分析进程发起的请求。它们能抓到数据,但最大的问题是"缺少上下文"。你看到一条POST请求,但不知道是点击哪个按钮触发的、页面处于什么状态、请求之间有什么前后依赖。更重要的是,很多现代前端项目用WebSocket或HTTP/2的流式传输,传统抓包工具的解析能力跟不上,而且遇到扫码登录、滑块校验这类交互流程时,纯网络层抓包基本束手无策。
所以主链路我选了Playwright来驱动浏览器。它的route拦截能力可以把浏览器发出的每一个请求都截下来,同时我知道当前页面URL、执行了哪些点击操作,上下文是完整的。Python和Node的生态都有对应的绑定,我这边主语言是Python,用的是playwright-python,后面所有示例都按这个生态来写。
需要说明的是,浏览器自动化做到的是"真实用户视角的流量捕获",它无法捕获绕过浏览器直接由原生客户端发出的流量,这是这套方案的边界所在。如果目标是移动端App的API逆向,得另找入口或者配合其他的流量捕获方式,这里不展开。
1.3 LLM在链路中的角色,不只是一个代码生成器
有些人会觉得,LLM在这个方案里不就是"读一段JSON然后写代码"吗?其实不是。它承担了三个独立职责:流量语义理解、schema推断、代码生成模板化适配。理解这一步,LLM要能从一堆URL和参数中识别出这是"用户登录接口""商品列表接口"还是"订单详情接口",并判断接口间的关联。推断这一步,LLM要分析响应JSON的结构,给出字段类型、可空性、嵌套关系,识别数组和枚举。生成这一步,则要把上述推断结果组装成目标语言的类型定义、请求函数、错误处理逻辑。
如果把三个职责混在一次提示词里让LLM全做完,效果很差。我第一版就是图省事,直接丢一个请求URL和响应体给LLM让它"写一个API客户端",结果它经常自己发挥出与真实响应不一致的字段,因为生成的依据不足。后来拆成多步:先做语义理解,再做schema推断,最后做代码生成。每一步的输入输出都经过结构化约束,LLM的自由发挥空间被压缩到最小。这个设计决策是整个方案中最关键的,后面第2章会更细地讲。
2. 核心链路一:浏览器自动化流量捕获与预处理
2.1 流量捕获的两个层级:Route拦截与运行时Hook
我实际用的方案里,流量捕获分了两路同时进行。一路走Playwright的page.route接口,适合捕获HTTP层面的常规请求,能看到请求头、请求体、响应体和状态码;另一路走运行时注入,用add_init_script在页面加载前注入脚本,重写window.fetch和XMLHttpRequest.prototype,适合捕获那些被框架内部逻辑处理掉、或者走异步时序比较特殊而route不容易拿全的请求。
为什么需要两路?因为route拦截在遇到某些使用了Service Worker或者流式读取的大响应时,会有明显的性能损耗,甚至影响页面正常运行。而运行时注入虽然拿到的是内存里的对象,少了一部分网络层面的细节(比如确切的下行字节数、HTTP版本),但胜在速度快、不阻断页面。我通常让运行时Hook作为主数据源,route拦截作为校验和兜底。两边数据合并时按"URL + 方法 + 关键参数"做指纹去重,这样同一接口在多次操作中反复调用的记录会被压缩成一条。
2.2 流量清洗:去动态、去噪声、做归一化
捕获到的原始流量是一个很长的列表,里面充斥着大量动态值和时间戳。直接把它丢给LLM分析,LLM会分不清哪些参数是业务参数、哪些是临时参数,schema推断也会被带偏。所以我在捕获之后加了一道清洗流水线,做四件事:
- 去动态参数:把请求URL、请求体中的token、sign、timestamp、nonce等字段标记为动态值,替换成占位符。初始版本用规则匹配,后来发现不同项目对这些参数的命名差异很大,规则维护成本高,就改成"先LLM识别候选动态字段+人工确认一次,随后按字段名记忆复用"的半自动方式。
- 去重复请求:按URL+方法+请求体内容的hash去重,同一个接口在循环里发了几十次的话,只保留一次。避免LLM看到的样本全是同一个模板的变体。
- 归一化请求体:把JSON里的键排序、去掉无意义的空字段。这样既能减少token开销,也能让LLM更容易识别结构。
- 拆分批量接口:有些前端会把多个操作合并到一次批量请求里,清洗时会把这种请求标记出来,单独加一轮分析,否则schema推断会混乱。
2.3 捕获样本的筛选策略
不是所有捕获到的流量都值得分析。脚本启动后,页面可能加载了大量静态资源、埋点上报、日志统计类的请求,这些接口对业务客户端生成没有价值。我的做法是维护一个过滤规则:排除静态资源后缀(.js、.css、.png、.woff2等),排除明显的事件上报类URL(包含collect、track、log、metric、analytics等关键词),再按响应体大小过滤掉过小或过大的条目。一般来说,最终进入LLM分析环节的请求数量会从原来的一两百条降到三四十条,质量却高得多。
这个筛选策略在不同项目上略有差异,比如一些系统把业务接口也放在/track路径下,这时候纯规则就会误杀。所以我把规则过滤设计成"可配置的预过滤器",允许在项目级配置里调整排除和保留的关键词,而不是写死在代码里。
2.4 完整的Playwright拦截示例
下面这段是实际使用的核心捕获代码,做了简化但保留了关键逻辑。它启动浏览器,打开目标站点,执行一串用户操作,同时把所有HTTP请求的元数据和响应结构记录下来。
import asyncio import json import hashlib from playwright.async_api import async_playwright captured = [] def fingerprint(method, url, body): raw = f"{method}|{url}|{body}" if body else f"{method}|{url}" return hashlib.sha1(raw.encode()).hexdigest()[:12] async def collect_flow(start_url, actions): async with async_playwright() as p: browser = await p.chromium.launch(headless=False) page = await browser.new_page() seen = set() async def on_request(route, request): try: body = request.post_data or "" fp = fingerprint(request.method, request.url, body) if fp not in seen: seen.add(fp) captured.append({ "method": request.method, "url": request.url, "post_data": sanitize_dynamic(body), "headers": {k: v for k, v in request.headers.items() if k.lower() in {"content-type", "accept"}}, "resource_type": request.resource_type, "fingerprint": fp, }) await route.continue_() except Exception: await route.continue_() await page.route("**/*", on_request) await page.goto(start_url) for action in actions: # actions是预先定义的操作序列,比如点击某个按钮、填写某个表单 await page.click(action["selector"]) await page.wait_for_timeout(1500) await browser.close() def sanitize_dynamic(raw_body): import re placeholders = { "token": "<TOKEN>", "sign": "<SIGN>", "timestamp": "<TS>", "nonce": "<NONCE>", } for key, ph in placeholders.items(): raw_body = re.sub(rf'"{key}"\s*:\s*"[^"]*"', f'"{key}": "{ph}"', raw_body) raw_body = re.sub(rf'"{key}"\s*:\s*\d+', f'"{key}": "{ph}"', raw_body) return raw_body这里有个注意点:page.route注册的位置要放在page.goto之前,否则第一个页面的请求会漏掉。actions用等待时间而不是等待元素出现,是为了在复杂页面里保持简单,代价是节奏固定。如果目标站点有较重的异步加载,建议用page.wait_for_selector替代固定延时,捕获准确率会高很多。
2.5 响应体的捕获与样例截断
清洗阶段只处理了请求侧,响应侧的规划同样重要。我把响应体的处理拆成两步:第一步在捕获时保存一个精简版本,去掉多余字段、只保留结构骨架;第二步在LLM分析前做样例截断,数组类型只保留两条完整元素和最后一条元素,中间的省略。这个截断策略对schema推断的影响很小,因为数组元素的类型只需要一两个样本就能判断出来,而截断能把token开销降到一个可接受的范围。
响应结构和请求参数是两条独立的处理路径。请求参数解决的是"调用方需要传什么",响应结构解决的是"调用方会拿到什么"。无论哪一侧发生变化,重新跑一遍捕获流程都能让schema自动更新。我实际维护这套方案的时候,最舒服的一点就是这种"点击几下游览器、然后等几分钟拿到新SDK"的迭代节奏。
3. 核心链路二:用LLM把流量记录解析成结构化API Schema
3.1 提示词模板的设计:分阶段、给约束、要JSON
LLM分析这一步,我把它拆成了三个阶段:语义理解、字段推断、代码生成。每个阶段用独立的提示词模板,输入逐步累积,输出逐步结构化。这样做的好处是能单独调优每个阶段,而且任何一个阶段出错时,我能定位到具体是LLM理解错了还是上游数据清洗有问题,不至于一锅端。
第一个阶段的提示词只要求LLM回答一个简单问题:这条请求属于什么业务操作,它的参数分别代表什么含义。输出格式被约束成一个固定JSON结构:
{ "operation": "brief description", "params": [ {"name": "id", "meaning": "用户ID", "dynamic": false} ], "dependencies": ["request_id"] }第二个阶段会带上"清洗后的请求记录 + 该接口的响应JSON样例 + 阶段一的语义理解结果",要求LLM输出完整的字段schema。这里最关键的是在提示词里明确几个约束:不能编造响应中没有出现的字段;必须区分字符串和可能为空的字段;数组要给出元素类型;嵌套对象要展开。光是这几条约束就能过滤掉九成以上的schema幻觉问题。
第三个阶段才把前面两阶段的JSON结果拼成一份"API描述文档",由LLM生成具体语言的客户端代码。整条链路里我踩过最大的坑,就是有人在第一个阶段就要求LLM输出代码,结果它把字段名、类型全都猜错。
3.2 多轮对话如何做上下文管理
分析几十个接口时,如果把每个接口都单独丢一次给LLM,速度慢且成本高。但放在同一个对话里让LLM连续分析,又容易产生混乱,可能把上一个接口的结论带到下一个接口里。我的做法是按业务域分组,一组一个对话上下文。所谓业务域,是根据URL前缀和页面功能自动划分的,比如把/user/、/account/开头的请求归为一组,把/order/开头的归为另一组。分组之后,对话里先放一组接口的语义理解结果,再让LLM输出这一组的完整schema,最后统一生成客户端。
这里要特别注意上下文长度的控制。响应体特别大的接口(比如列表接口带上百条样例数据)会在组内占用大量token,所以样例响应在进入LLM之前会先做截断:取前两条数据和最后一条数据,中间的省略。这个处理对schema推断的影响很小,因为数组元素的类型只需要一两个样本就能判断出来。
3.3 LLM输出的校验与纠错机制
光让LLM输出规范化JSON还不够,必须做二次校验。我在项目里加了一个pydantic模型做输出schema校验,LLM返回的JSON直接解析成这个模型的实例。解析失败时,会把错误信息回传给LLM,让它重新生成。这一步实际上形成了一个"生成-校验-重试"的闭环,实测能让LLM输出的可用率从一开始的八成提高到接近百分之百。
校验逻辑里最重要的一条是可空性推断。很多接口字段在空数据时会被省略,比如用户没有头像时干脆不返回avatar字段。LLM只分析一条响应样例时会漏掉这类"条件性字段",所以我在汇总多个响应样例时,会让LLM统计哪些字段在不同响应中出现了、哪些只出现过一次。只出现一次的字段统一标记为可空,这是一个非常实用的规则。
3.4 一个经过实战打磨的Schema推断提示词
下面是我实际在用的第二阶段提示词模板,比较容易迁移到别的项目里。现在写出来的版本做了脱敏和精简,核心约束都保留了。
你是一个API逆向工程师。下面我会给你一段清洗后的HTTP请求记录,以及该接口的响应JSON样例。 你需要推断出一个完整的API Schema,并只输出JSON,不要输出任何解释。 约束条件: 1. 只能使用响应样例中出现过的字段,禁止凭空补充。 2. 对于只出现在部分样例中的字段,nullable必须设为true。 3. 数组类型必须给出elements的具体类型,禁止使用any。 4. 嵌套对象必须展开,保留层级关系。 5. 字符串类型如果是枚举值,列举出你用到的候选值。 输出格式: { "endpoint": "POST /api/order/list", "summary": "订单列表查询", "request": { "query_params": [{"name": "page", "type": "integer", "required": true}], "body_params": [{"name": "status", "type": "string", "required": false}] }, "response": { "data": { "type": "object", "properties": { "items": {"type": "array", "elements": {"type": "object", "properties": {}}}, "total": {"type": "integer", "nullable": false} } } } }实际跑下来,这个模板产出的schema结构稳定度很高,最常见的问题反而是我上游给的响应样例本身太杂,比如同一个接口返回了两种完全不同的结构。这个问题不是提示词能解决的,需要在流量清洗阶段增加一个"响应结构分类"步骤,把两种结构分开处理。
3.5 Schema聚合:多个请求实例合并成一个接口定义
清洗和分组之后,同一个接口往往会存在多个请求实例,比如用户分页浏览时,page=1、page=2、page=3都各有一条记录。LLM分析时必须把这些实例合并成一个schema,否则会生成重复的客户端方法。
我是这样做的:把同一接口的多个请求实例按响应结构hash分组,hash相同则认为是同一结构,取其中响应字段最全的一条作为schema代表;hash不同则标记为"疑似多态接口",单独做一轮LLM分析。多态接口的情况在真实项目中不算罕见,比如一个泛型返回结构里包裹不同类型的数据,这种场景直接用规则处理非常痛苦,交给LLM分析反而能用自然语言把差异讲清楚。
3.6 接口之间关系的识别与依赖排序
除了单个接口的schema,生成客户端时还需要知道接口之间的调用关系。比如"获取订单详情"可能需要先"获取订单列表",然后在列表项里拿order_id再去调详情。这种依赖关系LLM能从URL路径、参数命名和页面操作顺序中推断出来,但推断结果不一定可靠。我的做法是生成一份依赖关系图,只把强依赖关系(一个接口的响应字段是另一个接口的必填参数)纳入代码生成,弱依赖关系只记录为注释提示。
依赖排序的意义在于,LLM生成代码时可以按照依赖层次组织文件结构,避免出现一个模块引用另一个还没有生成的模块。实际项目里我还加入了一个循环依赖检测:如果A需要B的参数、B又需要A的参数,就标记为人工确认。循环依赖大多数时候意味着这两个接口其实共享一个公共上下文,合并成一个客户端方法更合理。
4. 生成生产级API客户端的代码架构与细节
4.1 生成代码的目标语言与模板选择
我当前的项目里,生成的客户端主要面向TypeScript和Python两种语言。这两种语言的使用场景不同:TypeScript面向浏览器端和Node端调用,Python面向服务端脚本和数据采集任务。生成策略上,我没有让LLM从头写文件,而是准备了一套固定的模板骨架,LLM的任务是填充模板里的类型定义和方法实现。把"结构"和"内容"分离有几个好处:生成的代码风格始终统一;模板里的重试、超时、错误处理逻辑是经过验证的,不会被LLM改坏;后续如果要调整基础设施逻辑,只需要改模板,不用把几十个接口全部重新生成一遍。
这套思路和很多人一开始想的"让LLM直接生成一个完整SDK"不同。LLM直接生成完整SDK听起来很酷,但试过就知道,它会在一些边角逻辑上反复横跳,比如超时配置写到了错误类型里、重试判断条件写错等等。所以我把生成范围收缩到"类型定义+接口函数体",其余一切都交给代码模板,稳很多。
4.2 TypeScript客户端模板的关键实现
TypeScript模板里包含这样几个部分:一个HttpClient类封装fetch和重试逻辑;按业务域拆分的多个API模块文件;统一的错误类型定义;以及从schema推导出的interface。生成时的核心逻辑是把LLM输出的JSON schema转换成TypeScript类型,这一步通常是规则化的转换,不依赖LLM。
举个类型转换的例子,schema里的一个object属性会被转换成interface:
export interface OrderItem { order_id: string; user_id: string; amount: number; status: "pending" | "paid" | "cancelled"; created_at: string; products?: ProductItem[]; }对于可空字段,我统一使用可选属性?而不是| null,因为在JSON反序列化的场景里,字段不存在的概率远大于显式null的概率。这个决策是从实际API响应中统计出来的:很多后端在字段为空时直接省略键,所以把可空字段约束成可选属性,调用方的代码会自然地去判空,类型反而更安全。
4.3 请求签名与动态参数的回填策略
前面清洗阶段把token、sign这些动态参数替换成了占位符,那么在生成客户端时就必须提供一个回填机制。我的方案是模板里预留一个RequestContext接口,使用者需要在初始化时提供一个Provider,这个Provider负责从登录态或会话中获取token并填充到请求里。
export interface RequestContext { getToken(): Promise<string>; getSign(params: Record<string, unknown>): Promise<string>; }对应的生成代码里,所有涉及动态参数的地方都调用context的相关方法。这样做的好处是客户端代码本身不关心token从哪里来,生产环境接入时只需要实现一个Provider。我在实际项目里遇到过一个很隐蔽的问题:有些接口的token不是在请求头里,而是放在cookie中,模板对Cookie的序列化做了单独处理,否则生成的客户端在非浏览器环境会失败。
4.4 重试、超时、幂等等生产级细节
生产级客户端和demo代码最大的区别就在这些非功能需求上。我在这套生成体系里内置了几项默认策略:
- 超时:默认连接超时5秒,请求超时15秒,可通过构造参数覆盖。
- 重试:对5xx、429以及网络层超时做最多3次重试,采用指数退避加抖动,避免重试风暴。
- 幂等:GET请求天然幂等,直接重试;POST请求只对"明确是幂等操作"的接口重试,否则不自动重试,避免重复下单之类的事故。
- 日志:每个请求都输出方法、路径、耗时、状态码、错误摘要,错误摘要里不包含请求体,防止敏感信息泄漏。
这些策略全部固化在模板里,LLM生成的接口函数不需要关心重试逻辑,它的职责只是拼好URL、序列化参数、声明返回类型。生成体系的架构因此非常清晰:LLM管"接口长什么样",模板管"请求怎么做",两者通过一个确定的接口契约连接。
4.5 自动化验证:录制回放与响应对比
生成完客户端之后,不能只用类型检查通过就算完了。我加了一个验证环节,把清洗阶段留下的原始响应样例保存成回放数据,然后用生成的客户端去重新请求同一个接口,再拿真实响应和回放数据的结构做对比。结构对比不是简单比对JSON字符串,而是递归比对JSON的键集合和叶子节点类型。这个环节能发现两类问题:一类是schema推断错了,比如字段类型从string被推断成了number;另一类是动态参数回填没生效,请求直接被服务端拒绝。
验证通过后,我还会让LLM看一眼对比报告。如果存在差异,LLM会分析差异是"数据变化"还是"schema推断错误"——数据变化指的是字段值变了但类型没变,这类差异直接忽略;schema推断错误则需要把新信息回填到schema并重新生成客户端。这一步把整个流程变成了一个带反馈的闭环,自动化程度比"一次生成、人工review"高很多。
4.6 Python客户端的差异处理
Python端和TypeScript端有一个较大的差异:Python生态里普遍用类型注解,但运行时并不强制。我生成Python客户端时,保留了完整的类型注解,同时生成pydantic模型而不是纯dataclass。原因很简单:接口返回的数据在反序列化时需要进行类型校验,pydantic能直接报出字段类型不匹配的位置,这在排查schema推断错误时非常有用。
from pydantic import BaseModel from typing import Optional, List class OrderItem(BaseModel): order_id: str user_id: str amount: int status: str created_at: str products: Optional[List["ProductItem"]] = NonePython客户端的请求方法也统一使用httpx而不是requests,因为httpx原生支持异步,同时具备连接池和超时控制,和模板里的重试逻辑配合更自然。如果你的项目还在用requests,也不是不能用,但异步场景下早晚要换。
5. 实战中踩过的坑与排查经验
5.1 LLM幻觉字段问题,怎么从根上解决
幻觉字段是这套方案上线初期最头疼的问题。LLM在生成类型定义时,会把响应样例里根本没有的字段也写进去,比如我们只给它传了一个订单列表的响应,它却编造了"discount_amount"、"refund_info"这类字段。做录制回放验证时,这些多出来的字段会被标记为"响应中不存在的字段",于是开始追查。
排查下来,根因有两个。一是提示词里没有明确禁止编造字段,LLM会倾向于把响应结构补得更"完整"。二是同一组对话里混入了其他接口的响应样例,上下文污染导致它把A接口的字段搬到了B接口。修复方式对应也简单:提示词里加死"禁止补充未出现字段"的指令,同时严格保证同一组对话里的接口共用的是同一份清洗后的数据源。经过这两个修复,幻觉字段问题基本绝迹。
5.2 动态参数误识别导致客户端请求被拒
清洗阶段把字段名含token的参数全部替换成占位符,但有一次目标接口的业务参数里真的有一个叫token的字段,而且它的值是由业务逻辑算出来的固定值,不是什么会话凭证。替换掉之后,生成出来的客户端永远无法请求成功。这个问题让我意识到,动态字段识别不能只靠字段名,要结合字段值的特征:如果同一个值在多条请求中重复出现,更可能是业务固定值;如果每次都不同,才是真正的动态字段。后来我把判定逻辑改成了"字段名提示词+值重复度+请求时序一致性"三者结合,误判率大幅下降。
5.3 流式响应和WebSocket的处理边界
这套方案目前的边界在于流式响应和非HTTP流量。流式响应比如Chat接口的SSE类型,响应体不是一个完整JSON,schema推断逻辑直接失效。我的处理是短期的:检测到content-type为text/event-stream时,不进入schema推断链路,标记为"流式接口"并生成一个通用的流式消费客户端模板。WebSocket流量虽然捕获层能拦截到,但格式和HTTP差异太大,暂时没有纳入自动生成范围,仍然是半人工处理。
这个边界处理是我个人认为比较务实的做法。一个逆向工程工具不可能覆盖所有协议形态,把能自动化的部分做到极致,剩下的保持清晰的边界和可行的半自动方案,比硬塞进一条链路里更可靠。
5.4 成本与速度:批量分析时的Token优化
几十个接口全部交给LLM分析,token消耗是笔不小的开销。我做了几项优化。一是分组批量分析而不是逐接口独立对话,上下文复用让相同部分不被重复传输。二是样例截断,每个响应最多保留两条完整样例。三是schema缓存在本地,同一个接口只要响应结构没有变化,二次运行时直接命中缓存,不再调用LLM。实测下来,一个五十个接口的项目,全流程分析成本大约相当于直接把所有完整样例传进去的三分之一。
速度方面,瓶颈主要在大响应体的序列化和LLM生成代码的部分。前者靠流式解析解决了一部分,后者没什么好办法,只能靠并发调度多个LLM会话,把按业务域划分的接口分组并行发送。串行跑要十分钟的任务,并行后压到三分钟以内。
5.5 常见问题速查表
我把平时最常被问到、也是自己反复踩的几类问题整理成了一张速查表,方便读者在跑这套方案时快速对照:
| 现象 | 可能原因 | 处理方案 |
|---|---|---|
| 生成的schema里出现不存在的字段 | 提示词缺少禁止补充约束,或上下文混入其他接口 | 加约束指令,严格按业务域分组送LLM |
| 客户端请求被服务端拒绝 | 动态参数回填失效,或动态字段误识别 | 检查RequestContext实现,调整动态字段判定逻辑 |
| 同一接口生成多个重复方法 | 清洗阶段请求指纹去重失效 | 确认fingerprint包含完整请求体hash,核对筛选规则 |
| 响应结构时对时错 | 上游样例响应包含多种结构 | 增加响应结构分类步骤,多态接口单独分析 |
| Token消耗严重超预期 | 样例未截断,接口间上下文重复 | 启用样例截断策略,增加schema本地缓存 |
| 生成的代码编译不过 | 类型转换代码有边界情况未处理 | 用规则化转换处理schema到类型的映射,LLM只生成方法体 |
6. 这套方案能用在哪些地方,以及后续的扩展空间
6.1 最直接的应用场景
我目前看到的最有价值的应用场景有三类。第一类是内部系统对接,很多老系统没有API文档,或者文档严重滞后于实际代码,用这套方案一键生成客户端,等于让文档从代码里"长"出来。第二类是第三方站点的数据采集与自动化,生成出来的客户端比直接用Playwright跑页面稳定得多,接口变更时只需要重新跑一遍捕获流程。第三类是作为API兼容层的脚手架生成工具,迁移老服务时,先用这套方案把旧接口的定义全部摸清,生成对新旧两端的适配层,迁移过程中客户端代码基本不用动。
6.2 和现有工具链的配合方式
这套方案不需要替换任何已有组件。它生成的客户端遵循普通的HTTP调用约定,可以独立使用,也可以被现有框架集成。我在项目里就是把它和现有的服务端SDK并行使用了一段时间:新代码走生成的客户端,老代码走手写SDK,两边通过同一个接口定义文件做类型共享。过渡期结束之后,手写SDK删除,新客户端成为唯一实现。这种渐进式替换的方式风险最小,也最容易获得团队支持。
6.3 后续几个值得做的扩展方向
如果之后继续做下去,我的优先级大概是这样的。首先是支持OpenAPI/WSDL等已有契约格式的输入,让方案能同时处理"有文档的接口"和"没有文档的接口"。其次是增加结构差异对比的可视化界面,目前命令行输出的对比报告可读性一般。最后是把捕获、分析、生成、验证打包成CLI工具,让业务方在不了解内部细节的情况下也能使用。这几个方向做到哪一步算完,取决于实际项目的需求强度,但思路是清晰的:自动化程度还能更高,边界还能更广。
我再补一句实际体会。逆向工程这件事,最反人性的地方就是它把大量时间花在"观察并记住细节"上,而人的短期记忆和耐心都是有限的。LLM和浏览器自动化的组合真正改变的是工作模式:人只需要定义"要抓哪些流程",剩下的观察、推断、编码、验证由机器接手。我最近几次重新生成客户端,从跑完用户操作到拿到可用的TypeScript SDK,全程不到十五分钟,而手工做过同样事情的老读者应该知道,这活过去至少得搭进去大半天。
如果你也在做类似的API对接或者工具链建设,我的建议是先拿一个小项目试水:挑一个二十个接口左右的后台系统,跑通捕获、分析、生成、验证这四步,感受一下瓶颈在哪里。等跑顺了,再往更大规模的项目上铺开。技术方案本身不复杂,复杂的在于每个项目里那些独特的脏活,而这些东西,只有亲手踩一遍才知道怎么绕过去。