news 2026/10/20 10:00:26

大模型Tool Use手写实战:三种工具调用方案对比与选型指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型Tool Use手写实战:三种工具调用方案对比与选型指南

前两天面试,面试官翻着我的简历,忽然指着一行项目经历问:“你写 Agent 的时候,Tool Use 是怎么做的?现在手写一个出来我看看。”我第一反应是有点懵,平时都是用大模型 API 自带的函数调用能力,框架层封装得严严实实,谁还真手写过。好在我平时会拆框架源码,冷静了十几秒,在白板上把三种方案都写了出来。面试官看完没说话,最后在评分栏打了个勾。

先说说 Tool Use 是什么。简单讲,Tool Use 就是让大语言模型“决定调用哪个工具、参数是什么”,然后由外部代码去真正执行工具,把结果回传给模型的过程。相当于模型不直接回答,而是先“伸手”去拿数据,再基于数据作答。这个能力是现在所有 Agent 应用的地基,面试官让我手写,本质上是想确认我不是只会调用封装好的库,而是真明白模型和外部系统之间那条边界在哪。

这篇文章我就把当时写的三种方案完整还原出来,从最省心的原生方案,到不依赖模型特殊能力的“丐版”方案,再到让模型自己写代码执行的“野路子”。每种都会讲实现思路、关键代码、以及我踩过的坑。无论你是准备面试,还是想在私有化场景接一个稳定的工具调用链路,这套内容都能直接用。

1. 手写之前,先把 Tool Use 的核心机制想清楚

1.1 两段式流程:请求工具调用、执行并回传结果

Tool Use 看起来是个很性感的名字,但拆开其实就是两段式流程。

第一段叫“请求工具调用”:用户问了一句“北京今天适合穿什么”,模型在自己内部判断出需要查天气,于是它不会直接回答,而是输出一段结构化的指令,语义是“我要调用 get_weather 这个工具,参数是 city=北京”。注意,到这一步为止,模型并没有真正查到天气,它只是把“调用意图”用一种约定好的格式写了出来。

第二段叫“执行并回传结果”:外部代码收到这段结构化指令,去工具注册表里找到 get_weather 对应的真实函数,执行它,拿到“晴,26度,微风”这样的结果。然后这段结果被拼进对话历史,再次发给模型。模型看到工具返回的真实数据,再组织出给用户的最终回答。

我曾经用生活里的点菜来类比:用户是食客,模型是服务员,工具是后厨,工具结果是菜品。服务员自己不做菜,它只负责把客人需求写成菜单递给后厨,后厨做完菜,服务员再把菜端回给客人。Tool Use 就是那套菜单和传菜协议。

1.2 为什么模型能“输出”一次工具调用

面试时我把这个白板图画完,面试官问了一个好问题:模型本质上只是个文本生成器,它凭什么能“调用”工具?

答案其实很简单:模型确实没有真的调用任何东西,它只是被训练出了输出特定格式文本的能力。在训练阶段,模型看过大量“问题 + 工具描述 + 工具调用”的样本,学会了在合适的场景下输出{"name": "get_weather", "arguments": {"city": "北京"}}这样的文本。而我们说的“原生 Tool Call”,本质上是 API 服务端在生成阶段做了采样约束,让模型的输出必须落在合法工具名和合法参数格式里。

这也是为什么工具描述写得好不好,会直接影响调用成功率。模型是在做“下一个 token 预测”,它见过的工具描述越清晰、示例越充分,它就越不容易把参数写错。工具名是字符串,参数是字符串,调用决定也是字符串,一切都不过是“更聪明的自动补全”。

1.3 三种方案的选择逻辑

确认核心机制后,我在白板上列出了三个实现层次:

  • 方案一:用大模型服务商提供的原生工具调用接口,最正统、最稳。
  • 方案二:不用原生能力,靠 Prompt 约束模型输出 JSON,再解析执行。
  • 方案三:干脆让模型输出一段 Python 代码,在沙箱里执行。

这三种方案不是互相替代的关系,而是对应三种不同的现实场景。方案一适合生产环境,性能稳定;方案二适合私有化部署的本地模型、或者接口不支持原生工具调用的情况;方案三适合需要动态执行多重步骤、模型代码能力较强的场景。面试官想看的就是:你能不能在框架封装之外,自己把这套链路从零搭起来。

2. 方案一:模型原生 Tool Call,最省心的“正规军”

2.1 工具清单怎么写才不容易翻车

原生方案最关键的一步是定义工具清单。工具清单本质上是一份给模型看的“菜单”,用的是 JSON Schema 格式。拿查询天气举例,工具描述大概是这个样子:

[ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气情况", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:北京、上海" } }, "required": ["city"] } } } ]

这里有个容易被忽略的点:function.name要短、要语义明确,description要把这个工具“管什么、不管什么”说清楚。我见过很多调用失败的案例,不是因为代码写错了,而是因为两个工具的 description 边界太模糊,模型不知道到底该选哪个。比如“查询天气”和“查询温度”两个工具,一个说“获取城市天气”,一个说“获取城市气温”,模型很可能选错。

参数描述里同样要写清楚取值范围和格式。比如日期参数,要写明“格式为 YYYY-MM-DD”,否则模型可能给你输出“今天”或者“2024/5/3”这种没法直接用的东西。能用enum枚举的字段尽量用enum,比如天气单位只有“摄氏”“华氏”,枚举写死就能杜绝模型自创单位。

2.2 完整调用代码:从请求到执行回填

工具清单定义好之后,调用链路就是一个普通 HTTP 请求加一个循环。下面是我当时写的精简版:

import json import requests def call_model(messages, tools=None): resp = requests.post( "https://api.example.com/v1/chat/completions", headers={"Authorization": "Bearer YOUR_KEY"}, json={ "model": "some-model", "messages": messages, "tools": tools, "temperature": 0.1, }, timeout=30, ) resp.raise_for_status() return resp.json()["choices"][0]["message"] # 伪代码如下,核心是解析 tool_calls def handle_tool_calls(message, messages): for call in message.get("tool_calls", []): fn = call["function"] name = fn["name"] args = json.loads(fn["arguments"]) result = TOOL_REGISTRY[name](**args) # 真正执行工具 messages.append({ "role": "tool", "tool_call_id": call["id"], "content": json.dumps(result, ensure_ascii=False), })

第一次请求发出后,模型返回的message里如果带着tool_calls列表,说明模型决定要调工具了。这时候不能直接把结果丢给用户,而是要把工具执行结果作为一条role=tool的消息追加进对话历史,再调一次模型。注意这个tool_call_id字段必须对上,它是把“模型请求调用”和“工具执行结果”绑定起来的索引,丢了它模型就不知道这堆结果是谁的。

这个循环什么时候结束?当模型某次返回的message里没有tool_calls,只有正常文本内容,就说明它已经拿到足够信息,可以作答了。我把这个叫“无工具出口”。

2.3 原生方案的几个实操小心机

原生方案虽然最稳,但有几个细节新手容易踩。第一是并发工具调用。现在不少模型的 API 支持一次返回多个tool_calls,比如用户问“北京和上海哪个冷”,模型可能同时调两次get_weather。循环里逐个执行没问题,但要注意把每个结果独立追加到对话历史,顺序不能乱。

第二是content字段处理。很多模型在返回tool_calls时,content是空的。你把消息追加进历史时,不要把tool_calls丢了。有些封装库为了简单,只追加content,结果模型下一轮就失忆了,不知道刚才自己已经选了工具。

第三是temperature。工具调用场景我建议调到 0 或者 0.1。温度越高,模型越“天马行空”,可能把工具名从get_weather自动补全成get_weater。低温度可以显著减少这种低级幻觉。

# 完整回填代码 if message.get("tool_calls"): handle_tool_calls(message, messages) message = call_model(messages, tools) # 继续下一轮 print(message.get("content"))

3. 方案二:Prompt 约束 + JSON 输出,不依赖原生能力的“丐版”

3.1 为什么需要这种方案

方案一很好用,但依赖大模型 API 原生支持工具调用。实际工作中我碰到过不少尴尬情况:某个模型是通过第三方网关接入的,字段转换有 bug;或者公司私有化部署的是一个老版本模型,压根没开放工具调用参数。这时候方案二就派上用场了,我把这招叫“纯 Prompt 硬控”。

它的思路完全不依赖特殊接口:我把工具清单用自然语言写进 system prompt,然后要求模型输出严格格式的 JSON,再靠代码解析 JSON 来决定调什么工具。本质上是把“工具调用协议”从 API 层搬到了文本层。

3.2 把格式用 Prompt “焊死”

既然靠文本,那 Prompt 模板就是整个方案的生命线。我常用的模板是这样的:

你是一个只能通过工具回答问题的助手。 可用的工具如下: - get_weather(city: string): 查询指定城市的实时天气,返回温度、风力、天气现象。 - get_city_temp(city: string): 查询指定城市当前气温。 当我追问一个需要工具的问题时,你必须只输出以下格式的 JSON,不要输出任何其他内容: {"tool": "工具名", "arguments": {"参数名": "参数值"}} 示例: 用户:北京冷不冷? 助手:{"tool": "get_weather", "arguments": {"city": "北京"}} 如果用户的问题不需要工具,输出: {"tool": "none", "arguments": {}}

这个模板里有几个细节是反复调出来的。一是工具说明统一写成工具名(参数: 类型): 功能描述,模型对这种函数签名的理解力很强。二是必须给 few-shot 示例,模型是模仿高手,给它一个“用户问啥 → 助手输出啥”的对照,比写十行抽象指令管用得多。三是必须显式声明“不要输出任何其他内容”,否则模型会在 JSON 前后加“好的,我来查询一下”这种废话,增加解析难度。

3.3 解析容错:别被 JSON 里的“小尾巴”坑了

方案二写起来容易,最坑的是解析。模型输出看似是 JSON,实际上经常带各种意外:

{"tool": "get_weather", "arguments": {"city": "北京"}}\n\n希望对你有帮助!
{"tool": "get_weather", "arguments": {"city": "北京"}}

上面这两种情况我都遇到过。第一种是 JSON 后面跟了自然语言尾巴,第二种是模型把 JSON 包进了 Markdown 代码块。直接json.loads必挂。

所以我在解析前加了一道“提取”函数:找到第一个{和最后一个},截取中间内容再解析。最多情况下还要把被转义的引号还原。这个函数虽然土,但上线后解析成功率从 80% 提到了 99%。

import json def json_extract(text): start = text.find("{") end = text.rfind("}") if start == -1 or end == -1: raise ValueError("no json found") return json.loads(text[start:end + 1])

解析成功后,剩下的流程和方案一一致:查工具注册表,执行函数,把结果拼进对话历史,再发给模型。区别只是模型“决定工具”的载体从tool_calls变成了普通文本 JSON。

3.4 方案二的升级点:格式约束与动态工具列表

方案二还有个好处,就是对“模型能力”的要求很低。很多本地小模型不懂原生工具调用,但能读懂 Prompt 里的 JSON 格式说明。只是小模型更容易格式漂移,我建议做两手升级。

第一,如果模型服务商支持response_format之类的结构化输出参数,一定要打开,这等于让服务端帮忙强制 JSON 格式。第二,工具列表可以动态拼进 Prompt,用户问了什么问题,我就只把相关的三五个工具描述塞进去。工具少,模型选错的概率就低。工具多的时候,Prompt 会很长,干扰模型判断,反而容易乱来。

4. 方案三:让模型写代码,在沙箱里执行

4.1 原理与适用场景

到这里就有点“野”了。方案三的思路是:与其让模型输出工具调用指令,不如让模型直接输出一段 Python 代码,代码里自然就包含了工具调用逻辑。模型生成完代码后,我们拿到一个沙箱进程里执行,把执行输出回传给模型。

这个方案其实很多“代码解释器”类产品都在用。它特别适合多步骤任务。比如“查一下北京天气,再根据温度推荐三件出门单品”,如果走工具调用方案,模型可能要调两次工具中间还要做一次逻辑判断。但走代码方案,模型可以直接生成:

weather = get_weather("北京") print(weather)

严格说,这已经是 Agent 级别的行为了,模型不再逐个发工具请求,而是把多步逻辑写成了代码。

4.2 实现示例与注入风险警告

最简单的实现是这样:

import subprocess code = message.get("content", "") code = code.strip() if code.startswith("```python"): code = code[9:-3].strip() result = subprocess.run( ["python", "-c", code], capture_output=True, text=True, timeout=10, env={"PATH": "/usr/bin", "HOME": "/tmp"}, ) output = result.stdout.strip()

执行完把output作为工具结果回传模型,模型再基于真实输出来回答。

这里我必须强调一个红线:千万不要用exec直接在本地主进程执行模型生成的代码,哪怕模型是你自己接的,也很危险。模型有可能生成os.remove("/")或者subprocess.call(["rm", "-rf", ...]),这算轻的,严重的能把你服务器上的数据删光。我一般用subprocess.run起一个独立子进程,配合严格的超时,并且把环境变量清空,只留最基本的执行环境。如果要更安全,还得上容器隔离或者专用的代码沙箱运行时,这在生产环境里属于标配,不是可选项。

4.3 让代码质量起飞的小技巧

模型生成代码,最常见的问题是它“凭空捏造工具函数”。小模型可能直接写get_weather("北京"),而这个函数在沙箱里根本不存在。要缓解这个,我在 Prompt 里会给一段“可用的函数签名清单”,和方案二类似,但这次不是说“输出 JSON”,而是说“你可以直接调用这些已经定义好的函数,不要 import 任何其他库”。

另一个技巧是要求模型在代码里用print()输出关键结果,而且只输出结果不要输出过程指令。因为沙箱只会拿stdout回传给模型,模型写的调试日志、中间变量如果不截断,很容易把上下文塞爆。给stdout做长度限制是非常必要的。

方案三虽然最“狂野”,但它确实是三种方案里扩展性最强的。后面我如果想让 Agent 自己画图表、自己算数、自己批量处理数据,几乎都会落到这个方案上。工具调用方案是“一个动作一个动作来”,代码方案是“让模型把一套动作串成脚本一次跑完”。

5. 三套方案横向对比与选型建议

5.1 对比表格:成本、稳定性、安全性一次看全

面试到这一步,我在白板上画了下面这个表。

对比维度方案一(原生 Tool Call)方案二(Prompt + JSON)方案三(模型写代码)
实现成本低,但要求 API 支持低,几乎任何模型可用高,需要安全沙箱
调用稳定性高,格式有服务端保障中,靠 Prompt 约束低,代码质量波动大
模型要求支持原生工具调用能读指令并输出 JSON代码生成能力强
可控性高高低,执行过程难以一步步盯
安全性中,仍需校验参数中低,必须隔离执行
适用场景生产首选,Agent 标准架构私有化部署、受限模型多步动态任务、代码解释器

这个表不是我拍脑袋写的,是每次选型时我会问的三个问题决定的:模型支持什么?系统安全边界在哪?任务是要稳定还是要灵活?

5.2 我给团队定的选型建议

在绝大多数生产场景里,我用方案一打底。它是大模型服务商官方设计的协议,稳定性最高,维护成本最低。方案二专门留给那些没法上原生工具的私有化模型,团队内部接一些边缘自动化任务足够用了。方案三一般不直接放生产,除非做的是代码解释器这类产品型功能,那就要严肃地补全沙箱、资源限制、危险操作拦截,甚至人工审批流程。

另外不要死板地认为三种方案互斥。我现在最喜欢的架构是“混合”:主体用方案一,模型决定调工具时就走原生协议;遇到工具列表特别长、上下文很挤的场景,临时切方案二动态生成浓缩工具清单;某个特定任务需要算算术、遍历列表时,再让模型写一小段代码交给沙箱跑。一个系统里三种方案共存完全没毛病,关键是每一层都清楚自己在解决什么问题。

6. 实操:一套通用的 Tool Use 主循环封装

6.1 工具注册表:核心基础设施

不管选哪种方案,代码层一定需要一个工具注册表。注册表的作用很简单:把工具名映射到真实函数,同时存一份工具描述。

工具注册表让主循环代码变得极其干净。不管模型调的是天气工具、计算器还是数据库查询,执行逻辑都是同一行:TOOL_REGISTRY[name](**args)。新加一个工具时,只需要写一个普通函数,然后注册进去,主循环一行都不用改。

def get_weather(city: str): # 这里是伪代码,真实场景替换为天气服务请求 return {"city": city, "weather": "晴", "temp": 26} TOOL_REGISTRY = { "get_weather": get_weather, } TOOL_SCHEMAS = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称,例如北京"} }, "required": ["city"], }, }, } ]

6.2 主循环:一次讲透“多轮工具调用”

工具注册表就位后,剩下的核心就是主循环。这个循环是所有 Agent 类项目里最经典的那段逻辑:发请求 → 看有没有工具调用 → 有就执行回填 → 继续请求 → 没有就返回最终回答。

import json MAX_ITER = 5 # 限制最大迭代次数,防止死循环 def run_agent(user_input, messages=None): if messages is None: messages = [{"role": "user", "content": user_input}] for _ in range(MAX_ITER): message = call_model(messages, tools=TOOL_SCHEMAS) messages.append({ "role": "assistant", "content": message.get("content") or "", }) tool_calls = message.get("tool_calls") if not tool_calls: # 没有工具要调,说明模型已经有答案了 return message.get("content") for call in tool_calls: fn = call["function"] name = fn["name"] arguments = json.loads(fn["arguments"]) if name not in TOOL_REGISTRY: # 模型幻觉了一个不存在的工具,直接塞错误结果回去 result = {"error": f"tool {name} not found"} else: result = TOOL_REGISTRY[name](**arguments) messages.append({ "role": "tool", "tool_call_id": call["id"], "content": json.dumps(result, ensure_ascii=False), }) return "已达最大迭代次数,任务未完成"

有几个小细节我特意在这段代码里做了处理。第一是工具不存在时的兜底,直接把错误信息塞回给模型,让它自己改正或承认做不到,比程序直接抛异常体验好很多。第二是MAX_ITER一定要有,Tool Use 最容易出现的问题就是模型反复调用工具不收敛,比如一直查天气却总不回答用户“该穿什么”,没有上限这个请求会无限烧钱。第三是json.loads一定要包在 try 里,模型偶尔会在参数里多一个逗号。

6.3 关键参数把好关,少踩一半坑

参数这块我合并成一份速查心得。

temperature:工具调用阶段 0 到 0.2。模型“太有创意”时工具名容易写飞,回答阶段可以稍微回升到 0.5 左右。

max_tokens:不要给太小,工具调用结果经常包含 JSON、代码片段,生成到一半被截断会多一轮无意义的继续请求。我习惯给到 2000 以上。

timeout:单次 HTTP 请求超时 30 秒,工具函数自身也要有超时,尤其外部接口类的工具。有的外部服务慢起来能卡 5 分钟,别让一个工具拖垮整个对话。

MAX_ITER:控制在 5 到 8 次够用。超过 8 次还不收敛的任务,大概率是 Prompt 或工具描述有问题,而不是模型能力问题。

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

7.1 JSON 解析失败:永远绕不开的坑

方案二和方案三都会遇到解析问题。模型输出的 JSON 要么带 Markdown 代码块,要么带多余说明文字,要么参数里混了中英文标点。我的排查顺序是:先看原始输出是什么,再用json_extract提取,最后如果多次失败就追加一条 system 消息,告诉模型“刚才你的输出格式不对,请只输出 JSON”。实测最后这招很有用,模型有自纠错能力。

7.2 模型乱选工具或幻觉工具名

排在第一的常见问题是工具描述写得太像。两个工具如果都能“查天气”,模型就会随机选。建议把工具职责写窄,比如“拉取当前温度”和“获取 24 小时天气预报”两个 tool 分开,描述里明确“温度不包含预报,预报不包含当前温度”。幻觉不存在的工具名时,除了在代码里兜底,还可以在 Prompt 里加一句“只允许从给定工具列表中选择,不要猜测不存在的工具名”。

7.3 上下文越滚越长,调用几轮就超限

工具执行结果动不动几百上千字符,几轮下来上下文就爆了。我常用的三板斧:工具结果只保留必要字段,执行完立即做截断,比如只留前 500 字符;对历史消息做滑动窗口,只带最近 10 条;如果工具返回的是大段数据集,把它转成摘要或者 embedding 后再决策要不要带完整内容。在上下文长度受限的模型上,截断工具结果是最立竿见影的优化,没有之一。

7.4 工具副作用:写操作要“先问再做”

工具不只是查询,还有发邮件、改订单、删文件这类高风险操作。Tool Use 最大的安全隐患是模型一旦被误导,可能拿着用户的隐私数据去调危险工具。我的做法是:工具注册表里给每个函数标注is_dangerous,主循环执行前,如果目标是写类工具,先返回一个“二次确认”结果给模型,让它先向用户征求确认,用户点头后再真正执行。如果是代码方案,危险操作直接走人工审批,宁慢勿快。

注意:这里说的“人工审批”,指在代码层面设计一个等待用户确认的机制,不是真实业务流程和效力描述,只作为工具调用的安全设计参考。

最后再分享一点体会

回想整个过程,我最想说的是:面试官让你手写 Tool Use,真不是考你背接口,而是看你能不能站到“模型负责什么、代码负责什么”这条边界上去思考。模型只负责给出调用意图,代码负责真实执行,这两个角色一旦混乱,系统就会变成一个会编瞎话的“假执行器”。

我当时踩过最深的坑,是早期做一个模拟数据查询时,为了让测试效果好,直接在工具执行函数里写死了返回结果。结果模型每次都“查”到一个一样的答案,我还以为是 AI 变聪明了,其实是工具层造假。所以后来我给团队立了条规矩:工具的返回值必须来自真实执行,哪怕结果是空、是报错,都要如实回传。模型能接受错误,但不能接受欺骗,因为错误还可以容忍重试,假数据模型会一本正经地胡说八道。

如果你正准备把 Tool Use 落地到自己的应用里,我的建议很简单:先按文中的主循环跑通一个最小原型,再按生产要求补上参数校验、超时、安全沙箱这三件套。这套基础打扎实之后,再往这个骨架上挂记忆、路由、多智能体协作都不会慌。最后补一个实用技巧:调试工具调用时,打开“原始消息日志”,看到每轮 assistant 返回的完整 tool_calls 和 tool 消息,几乎所有诡异问题都能在这个日志里找到答案。

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

NeptuneAI实验追踪实战:从指标上报到跨实验归因

1. 项目概述:为什么一篇“博客翻译”值得单独成文?“NeptuneAI 博客中文翻译(二十八)”——光看标题,你可能下意识觉得这是个“搬运工活儿”:找原文、开翻译软件、润色、发出来。但在我过去十年持续跟踪AI基…

作者头像 李华
网站建设 2026/10/20 9:59:05

C++与Qt实现高精度弹道仿真:稳定性、确定性与实时渲染

简介:这是一套基于C与Qt6实现的跨平台弹道计算软件源码及可执行程序,面向军事仿真、射击训练、弹道建模等领域的开发者与科研人员,解决Python弹道库在高性能实时计算与桌面部署场景下的性能与封装瓶颈。资源包含79个文件,涵盖11个…

作者头像 李华
网站建设 2026/10/20 9:58:46

Java在AI时代并未掉队:Spring AI与工程化落地解析

最近和几个做后端的开发者聊天,话题不约而同滑向同一个焦虑:“Spring AI Alibaba 好像停更了,Java 在 AI 这条路上是不是要掉队了?”我没法给一个拍胸脯的保证,但可以把这件事拆开讲清楚。先说我的判断:某个…

作者头像 李华
网站建设 2026/10/20 9:58:11

NSGA-Ⅲ算法在梯级水火电联合多目标调度中的Matlab实现与实战

做电力调度优化的朋友应该都有同感:纯水电优化相对好处理,纯火电优化也相对好处理,可一旦把梯级水电和火电机组放在一起做联合多目标调度,问题复杂度立刻上了一个台阶。上游电站放多少水、下游电站能发多少电、火电该在什么时段多…

作者头像 李华
网站建设 2026/10/20 9:58:10

Playwright-MCP实战:用自然语言让AI驱动浏览器自动化测试

抛开包装过的宣传话术,Playwright-MCP这套组合,是我近一年在浏览器自动化方向摸过的最顺手的新玩法。核心就一句话:它把Playwright已有的浏览器控制能力,通过MCP协议暴露给AI助手,让你不再一行行写选择器、等元素、铺断…

作者头像 李华