news 2026/10/7 21:22:07

知识点16:案例实战:LangChain + MCP 打造任务分解Agent

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
知识点16:案例实战:LangChain + MCP 打造任务分解Agent

1. 为什么任务分解 Agent 总是「拆得漂亮、跑不起来」

复杂目标交给大模型直接做,结果往往是「看起来对、细节全错」。比如你说「帮我调研一下 2024 年国内新能源车出口情况,整理成一份带结论的报告」,模型会一口气写出一大段文字,但里面数据来源不明、结构混乱、还没法复用。真正能落地的做法,是先把目标拆成可执行的子任务,再让每个子任务去调用具体工具,最后把结果拼回来。

这就是任务分解 Agent 要解决的问题:它把「一个复杂目标」变成「一串可执行动作」。适合谁?适合正在做自动化工作流、智能客服、数据分析助手、报告生成这类场景的开发者。你不需要从零训练模型,只要用 LangChain 做编排、用 MCP 把工具标准化,就能搭出一个能跑通的任务分解流程。

我试过直接让模型「一步到位」,结果它在多步依赖的任务上经常漏步骤。后来改成「先分解、再执行、后整合」三段式,稳定性明显提升。下面这篇就按这个思路,把 LangChain + MCP 的任务分解 Agent 从环境准备到跑通验证完整走一遍,代码可以直接复制。

核心检索词先明确:LangChain 任务分解 Agent、MCP 工具接入、子任务编排、结果整合。这几个词会贯穿全文,你照着做就能复现。

2. TaoToken 前置准备:把模型调用和 MCP 工具链接通

在写 Agent 之前,先把「模型从哪来」和「工具怎么接」这两件事定下来。任务分解 Agent 对模型的要求是:能稳定输出结构化子任务列表、能按 ReAct 格式调用工具。所以你需要一个兼容 OpenAI 接口的模型服务,以及一套标准的 MCP 工具接入方式。

2.1 为什么用 TaoToken 做模型入口

LangChain 的ChatOpenAI默认走 OpenAI 接口。如果你想让同一套代码切换不同模型、又不想改业务逻辑,最省事的办法是把base_url指向一个兼容 OpenAI 协议的服务。TaoToken 提供的就是这种兼容入口,模型对话、Coding Plan、API Keys 都在一个控制台里管理。

官网地址:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= API 地址:https://taotoken.net/api

注意:API 地址后面不加任何参数,直接作为base_url使用。模型对话入口在 https://taotoken.net/api/chat,Coding Plan 在 https://taotoken.net/api/coding-plan,控制台在 https://taotoken.net/api/console,API Keys 管理在 https://taotoken.net/api/api-keys,接入文档在 https://taotoken.net/api/doc。Claude Code 相关接入看 https://taotoken.net/api/claude-code-anthropic。

2.2 拿 Key 与配置环境变量

先去 API Keys 页面创建一个 Key,然后写进环境变量,别硬编码在代码里:

export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

如果你用.env文件管理,就写成:

TAOTOKEN_API_KEY=sk-你的key TAOTOKEN_BASE_URL=https://taotoken.net/api

2.3 MCP 工具接入的两种方式

MCP(Model Context Protocol)本质是把工具能力标准化,让 Agent 用统一协议调用。落地时有两种接法:

第一种是本地 MCP Server,用 stdio 通信,适合文件操作、本地脚本这类工具。第二种是远程 MCP Server,用 HTTP/SSE 通信,适合搜索、翻译、存储这类独立服务。本文的示例用第二种,因为更容易验证和调试。

如果你用 Cline 或 Claude Code 这类客户端,MCP 配置通常写在settings.json或mcp.json里。以 Cline 的 MCP 配置为例,路径一般在~/.cline/mcp.json:

{ "mcpServers": { "task-tools": { "command": "python", "args": ["-m", "mcp_server.task_tools"], "env": { "TAOTOKEN_API_KEY": "sk-你的key" } } } }

如果你用 Codex,认证信息写在~/.codex/auth.json,格式如下:

{ "OPENAI_API_KEY": "sk-你的key", "OPENAI_BASE_URL": "https://taotoken.net/api" }

这里要记住三件套:Base URL、Key、Model ID。任何 MCP 客户端接入时,这三项必须同时给全,缺一个就会报 401 或连接失败。Model ID 按你实际使用的模型填,比如gpt-4o-mini或claude-3-5-sonnet,具体以接入文档里的模型列表为准。

2.4 安装依赖

pip install langchain langchain-openai fastapi uvicorn requests pydantic python-dotenv

装完后确认版本:

python -c "import langchain; print(langchain.__version__)"

版本太旧会导致initialize_agent的导入路径变化,建议用较新的稳定版。到这里,模型入口和工具接入的前置就完成了,接下来进入可复制的配置环节。

3. 可复制配置:LangChain + MCP 任务分解 Agent 完整代码

这一节是全文的核心,所有代码都可以直接复制运行。整体结构分三层:MCP 服务层提供搜索、翻译、存储三个工具;LangChain 编排层负责分解、执行、整合;Agent 入口把三者串起来。

3.1 MCP 服务层:三个基础工具

先写搜索服务,保存为search_mcp_service.py:

from fastapi import FastAPI, HTTPException from pydantic import BaseModel import uvicorn app = FastAPI(title="搜索MCP服务") class SearchRequest(BaseModel): query: str class SearchResult(BaseModel): content: str class MockSearchEngine: def search(self, query: str) -> str: db = { "2024年新能源车出口": "2024年国内新能源车出口约120万辆,同比增长约30%,主要出口地为欧洲和东南亚。", "全球气候变化趋势": "全球平均气温较工业化前升高约1.1°C,极端天气事件增加。", "可再生能源发展": "2024年可再生能源占全球新增发电装机容量80%以上,太阳能和风能主导。", } return db.get(query, f"未找到关于'{query}'的搜索结果。") engine = MockSearchEngine() @app.post("/mcp/search", response_model=SearchResult, tags=["MCP服务"]) async def search_mcp_service(request: SearchRequest): try: return SearchResult(content=engine.search(request.query)) except Exception as e: raise HTTPException(status_code=500, detail=f"搜索服务执行失败: {str(e)}") if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8001)

翻译服务保存为translate_mcp_service.py:

from fastapi import FastAPI, HTTPException from pydantic import BaseModel import uvicorn app = FastAPI(title="翻译MCP服务") class TranslateRequest(BaseModel): text: str source_language: str = "auto" target_language: str = "en" class TranslateResult(BaseModel): translated_text: str class MockTranslator: def translate(self, text: str, source_language: str, target_language: str) -> str: if target_language == "en" and "你好" in text: return "Hello!" if target_language == "zh" and "Hello" in text: return "你好!" return f"[Translated from {source_language} to {target_language}]: {text}" translator = MockTranslator() @app.post("/mcp/translate", response_model=TranslateResult, tags=["MCP服务"]) async def translate_mcp_service(request: TranslateRequest): try: result = translator.translate(request.text, request.source_language, request.target_language) return TranslateResult(translated_text=result) except Exception as e: raise HTTPException(status_code=500, detail=f"翻译服务执行失败: {str(e)}") if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8002)

存储服务保存为storage_mcp_service.py:

from fastapi import FastAPI, HTTPException from pydantic import BaseModel import uvicorn, json, os from typing import Dict, Optional app = FastAPI(title="存储MCP服务") class StoreRequest(BaseModel): key: str value: str namespace: str = "default" class RetrieveRequest(BaseModel): key: str namespace: str = "default" class StoreResult(BaseModel): success: bool message: str class RetrieveResult(BaseModel): value: Optional[str] = None exists: bool class MockStorageEngine: def __init__(self, storage_file: str = "mock_storage.json"): self.storage_file = storage_file if not os.path.exists(self.storage_file): with open(self.storage_file, "w", encoding="utf-8") as f: json.dump({}, f) def _load(self) -> Dict[str, Dict[str, str]]: try: with open(self.storage_file, "r", encoding="utf-8") as f: return json.load(f) except Exception: return {} def _save(self, data): with open(self.storage_file, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def store(self, key, value, namespace="default") -> bool: data = self._load() data.setdefault(namespace, {})[key] = value self._save(data) return True def retrieve(self, key, namespace="default") -> Optional[str]: data = self._load() return data.get(namespace, {}).get(key) engine = MockStorageEngine() @app.post("/mcp/store", response_model=StoreResult, tags=["MCP服务"]) async def store_mcp_service(request: StoreRequest): try: engine.store(request.key, request.value, request.namespace) return StoreResult(success=True, message=f"已存储到 {request.namespace}/{request.key}") except Exception as e: raise HTTPException(status_code=500, detail=f"存储服务执行失败: {str(e)}") @app.post("/mcp/retrieve", response_model=RetrieveResult, tags=["MCP服务"]) async def retrieve_mcp_service(request: RetrieveRequest): try: value = engine.retrieve(request.key, request.namespace) return RetrieveResult(value=value, exists=value is not None) except Exception as e: raise HTTPException(status_code=500, detail=f"检索服务执行失败: {str(e)}") if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8003)

3.2 LangChain 编排层:分解 + 执行 + 整合

保存为langchain_orchestrator.py。这里用ChatOpenAI指向 TaoToken 的兼容入口:

import os import requests from langchain_openai import ChatOpenAI from langchain.prompts import PromptTemplate from langchain.chains import LLMChain from langchain.agents import AgentType, initialize_agent, Tool llm = ChatOpenAI( temperature=0, model_name="gpt-4o-mini", openai_api_key=os.getenv("TAOTOKEN_API_KEY"), openai_api_base=os.getenv("TAOTOKEN_BASE_URL"), ) class MCPServiceClient: def __init__(self, base_urls: dict): self.base_urls = base_urls def call_service(self, service_name: str, params: dict) -> dict: if service_name not in self.base_urls: return {"error": True, "message": f"未知的MCP服务: {service_name}"} url = f"{self.base_urls[service_name]}/mcp/{service_name}" try: resp = requests.post(url, json=params, timeout=30) if resp.status_code == 200: return {"error": False, "result": resp.json()} return {"error": True, "message": f"状态码 {resp.status_code}", "details": resp.text} except Exception as e: return {"error": True, "message": "调用异常", "details": str(e)} mcp_client = MCPServiceClient({ "search": "http://localhost:8001", "translate": "http://localhost:8002", "store": "http://localhost:8003", "retrieve": "http://localhost:8003", }) def search_tool(query: str) -> str: r = mcp_client.call_service("search", {"query": query}) return r["result"]["content"] if not r["error"] else f"搜索失败: {r['message']}" def translate_tool(input_text: str) -> str: text, target = input_text, "en" if ";" in input_text and ":" in input_text: params = dict(p.split(":", 1) for p in input_text.split("; ") if ":" in p) text = params.get("文本", text).strip() target = params.get("目标语言", target).strip() r = mcp_client.call_service("translate", {"text": text, "target_language": target}) return r["result"]["translated_text"] if not r["error"] else f"翻译失败: {r['message']}" def store_tool(input_data: str) -> str: key, value, namespace = "", "", "default" if ";" in input_data and ":" in input_data: params = dict(p.split(":", 1) for p in input_data.split("; ") if ":" in p) key = params.get("键", "").strip() value = params.get("值", "").strip() namespace = params.get("命名空间", "default").strip() elif ":" in input_data: key, value = [x.strip() for x in input_data.split(":", 1)] else: return "存储失败: 格式应为 '键:值'" r = mcp_client.call_service("store", {"key": key, "value": value, "namespace": namespace}) return r["result"]["message"] if not r["error"] else f"存储失败: {r['message']}" def retrieve_tool(input_data: str) -> str: key, namespace = input_data.strip(), "default" if ";" in input_data and ":" in input_data: params = dict(p.split(":", 1) for p in input_data.split("; ") if ":" in p) key = params.get("键", key).strip() namespace = params.get("命名空间", namespace).strip() r = mcp_client.call_service("retrieve", {"key": key, "namespace": namespace}) if r["error"]: return f"检索失败: {r['message']}" res = r["result"] return f"检索成功: {res['value']}" if res["exists"] else f"键 '{key}' 不存在" tools = [ Tool(name="Search", func=search_tool, description="搜索信息,输入为查询词。"), Tool(name="Translate", func=translate_tool, description="翻译文本,输入格式 '文本:内容;目标语言:en'。"), Tool(name="Store", func=store_tool, description="存储数据,输入格式 '键:键名;值:值;命名空间:ns'。"), Tool(name="Retrieve", func=retrieve_tool, description="检索数据,输入格式 '键:键名;命名空间:ns'。"), ]

3.3 任务分解提示词模板

分解质量直接决定后续执行效果。模板要约束三件事:子任务数量、可执行性、工具匹配:

DECOMPOSE_TEMPLATE = """ 你需要将以下复杂任务分解为多个简单的子任务:{task} 请严格按以下格式输出,每行一个子任务: 1. 子任务1:描述 2. 子任务2:描述 ... 约束: - 子任务必须具体、可执行,避免"分析一下"这类模糊描述 - 顺序合理,后面的子任务可依赖前面的结果 - 只能使用这些工具:Search、Translate、Store、Retrieve - 子任务数量不超过 8 个 - 不要输出解释性文字,只输出编号列表 """

3.4 Agent 入口:串起三段式流程

class TaskDecomposer: def __init__(self, llm): self.chain = LLMChain(llm=llm, prompt=PromptTemplate( input_variables=["task"], template=DECOMPOSE_TEMPLATE)) def decompose(self, task: str) -> list: result = self.chain.run(task) subtasks = [] for line in result.strip().split("\n"): line = line.strip() if line and line[0].isdigit() and "." in line: parts = line.split(".", 1) if len(parts) > 1: subtasks.append({"number": parts[0].strip(), "description": parts[1].strip()}) return subtasks class ResultIntegrator: def __init__(self, llm): self.chain = LLMChain(llm=llm, prompt=PromptTemplate( input_variables=["original_task", "decomposed_tasks", "execution_results"], template="""基于以下信息给出完整最终结果: 原始任务:{original_task} 子任务:{decomposed_tasks} 执行结果:{execution_results} 请整合为连贯、详细的最终答案。""")) def integrate(self, original_task, decomposed_tasks, execution_results) -> str: tasks_str = "\n".join(f"{t['number']}. {t['description']}" for t in decomposed_tasks) results_str = "\n".join( f"子任务{i+1} ({r['task']}): {r['result']}" for i, r in enumerate(execution_results)) return self.chain.run({ "original_task": original_task, "decomposed_tasks": tasks_str, "execution_results": results_str, }) class TaskDecomposingAgent: def __init__(self, llm, tools): self.decomposer = TaskDecomposer(llm) self.integrator = ResultIntegrator(llm) self.executor = initialize_agent( tools=tools, llm=llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True) def run(self, task: str) -> str: print(f"接收到任务:{task}") subtasks = self.decomposer.decompose(task) print(f"分解得到 {len(subtasks)} 个子任务") results = [] for t in subtasks: print(f"执行子任务 {t['number']}:{t['description']}") r = self.executor.run(t["description"]) results.append({"task": t["description"], "result": r}) return self.integrator.integrate(task, subtasks, results) if __name__ == "__main__": agent = TaskDecomposingAgent(llm, tools) print(agent.run("搜索2024年新能源车出口数据,翻译成英文,并存储到 export_2024 键中。"))

这份配置里,Base URL、Key、Model ID 三件套都在ChatOpenAI初始化时给全了,MCP 工具通过MCPServiceClient统一调用。复制后改一下 Key 就能跑。

4. 验证请求:从启动服务到看到成功结果

配置写完,接下来验证整条链路是否通。分三步:先起 MCP 服务,再单独测工具,最后跑完整 Agent。

4.1 启动三个 MCP 服务

开三个终端,分别执行:

python search_mcp_service.py python translate_mcp_service.py python storage_mcp_service.py

看到Uvicorn running on http://0.0.0.0:8001这类输出就说明起来了。如果端口被占用,改uvicorn.run里的 port 即可。

4.2 用 curl 单独验证工具

先测搜索服务:

curl -X POST http://localhost:8001/mcp/search \ -H "Content-Type: application/json" \ -d '{"query": "2024年新能源车出口"}'

预期返回:

{"content": "2024年国内新能源车出口约120万辆,同比增长约30%,主要出口地为欧洲和东南亚。"}

再测存储和检索:

curl -X POST http://localhost:8003/mcp/store \ -H "Content-Type: application/json" \ -d '{"key": "export_2024", "value": "120万辆", "namespace": "default"}' curl -X POST http://localhost:8003/mcp/retrieve \ -H "Content-Type: application/json" \ -d '{"key": "export_2024", "namespace": "default"}'

检索应返回{"value": "120万辆", "exists": true}。这一步通了,说明 MCP 服务层没问题。

4.3 跑完整 Agent

python langchain_orchestrator.py

正常输出会经历三个阶段:先打印分解出的子任务列表,再逐个打印执行日志(ReAct 的 Thought/Action/Observation),最后输出整合后的最终答案。你会看到类似:

接收到任务:搜索2024年新能源车出口数据,翻译成英文,并存储到 export_2024 键中。 分解得到 3 个子任务 执行子任务 1:使用 Search 工具搜索"2024年新能源车出口" 执行子任务 2:使用 Translate 工具将结果翻译成英文 执行子任务 3:使用 Store 工具存储到 export_2024 ... 最终结果:2024年国内新能源车出口约120万辆...

如果最终结果里包含了搜索到的数据、翻译后的英文、以及存储成功的确认,说明整条链路跑通了。这时候你可以把任务换成更复杂的,比如「搜索气候变化和可再生能源两条信息,翻译后分别存储」,观察 Agent 是否能正确拆成 4 到 5 个子任务。

4.4 验证模型入口是否正常

如果 Agent 卡在分解阶段不动,先单独验证模型入口:

curl -X POST https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "你好"}]}'

能返回正常内容,说明 Key 和 Base URL 没问题。这一步是排查模型侧问题的基准线。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

跑不通的时候,报错信息往往很具体。下面按真实遇到的错误逐条对照。

5.1 401 Unauthorized

最常见。原因通常是 Key 没读到或 Base URL 写错。检查两点:环境变量是否真的导出成功(echo $TAOTOKEN_API_KEY),以及openai_api_base是否写成了https://taotoken.net/api。注意末尾不要多加/v1或斜杠,否则会拼出错误路径。如果你用.env,确认代码里调用了load_dotenv()。

5.2 local proxy failed

这个报错一般出现在 MCP 客户端连接本地服务时。含义是客户端尝试走本地代理但没连上。排查顺序:先确认 MCP 服务进程是否在跑(lsof -i:8001),再确认mcp.json里的command和args路径是否正确。如果你用的是远程 MCP Server,检查base_urls里的地址是否可达。注意:这里说的「代理」是本地进程通信层面的概念,不是网络访问层面的,别混淆。

5.3 reading 'choices' 报错

典型信息是TypeError: Cannot read properties of undefined (reading 'choices')。这说明模型返回体里没有choices字段,通常是接口返回了错误信息但代码没处理。原因可能是:模型 ID 写错、请求体格式不对、或者 Base URL 指向了一个不兼容 OpenAI 协议的端点。解决办法是先按 4.4 的 curl 验证模型入口,确认返回体结构,再回头检查ChatOpenAI的参数。

5.4 OAuth 相关报错

如果你用 Claude Code 或 Codex 这类客户端,可能会遇到 OAuth token 过期或未授权的提示。这类客户端通常有自己的认证文件,比如 Codex 的~/.codex/auth.json。检查里面的OPENAI_API_KEY和OPENAI_BASE_URL是否与当前使用的 Key 一致。如果换了 Key,记得同步更新这个文件,否则客户端还在用旧凭证。

5.5 子任务解析为空

Agent 跑起来但decompose返回空列表,说明模型输出格式不符合模板。原因通常是提示词约束不够强,模型加了额外解释文字。解决办法是在模板里强调「只输出编号列表」,并在解析时做容错:如果某行以数字开头但没匹配到.,尝试用其他分隔符。另外把temperature设为 0 能显著提升格式稳定性。

5.6 MCP 工具调用超时

如果 Agent 卡在某个子任务很久,多半是 MCP 服务没响应。给requests.post加上timeout=30,并在call_service里捕获超时异常返回明确错误。这样 Agent 不会一直等,而是把错误信息带回给模型,让它决定是否重试或换工具。

对照这几条排查,大部分接入问题都能定位。记住三件套:Base URL、Key、Model ID,任何一项缺失或不一致都会导致连接失败。

6. 把任务分解 Agent 用起来:从验证到长期运行

跑通一次验证只是开始,真正要用起来还得考虑稳定性和复用。这里给几个实操建议。

第一,把 MCP 服务做成可配置的。现在base_urls是硬编码的,实际部署时应该从环境变量或配置文件读取,这样换环境不用改代码。第二,给分解提示词加上「工具能力说明」,把每个工具能做什么、输入格式是什么写清楚,模型拆出来的子任务会更贴合实际工具。第三,执行阶段加日志落盘,每个子任务的输入输出都记下来,出问题时能回溯是哪一步偏了。

如果你要长期跑编码类或 Agent 类任务,可以关注 Coding Plan 入口:https://taotoken.net/api/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。它适合需要持续调用模型、做多轮编排的场景。模型对话入口在 https://taotoken.net/api/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,适合先验证模型输出质量。API Keys 管理在 https://taotoken.net/api/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,接入文档在 https://taotoken.net/api/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。

最后说一个我踩过的坑:任务分解 Agent 最容易出问题的地方不是模型,而是子任务之间的依赖没处理好。比如「先搜索再翻译再存储」这种链式依赖,如果分解时顺序错了,后面全乱。解决办法是在提示词里明确要求「按依赖顺序排列」,并在执行阶段加一个简单的依赖检查——如果某个子任务的输入依赖前一个的输出,就在描述里标注出来。这样即使模型偶尔拆错,你也能快速发现。

把上面这套配置跑通后,你可以把搜索服务换成真实 API、把存储换成数据库,整个 Agent 的骨架不用动。这就是用 LangChain 做编排、用 MCP 做工具标准化的价值:换工具不改逻辑,换模型不改代码。

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

云端服务启动后浏览器访问失败:先查端口链路还是重装应用?

云端服务启动后浏览器访问失败:先查端口链路还是重装应用? 云端 Web 服务排查里,最常见的误判之一就是: 看到服务已经启动,就默认浏览器应该可以访问。 这两个状态其实不是一回事。 “服务启动”说明服务进程或服务本身…

作者头像 李华
网站建设 2026/10/7 21:11:17

GHelper:3 个前置确认,10 分钟替代奥创中心

GHelper:3 个前置确认,10 分钟替代奥创中心 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenbook, E…

作者头像 李华
网站建设 2026/10/7 21:10:38

Agent-Reach 实战:为 AI Agent 构建浏览器触达层

1. 从零认识 Agent-Reach:它到底解决什么问题第一次看到 Agent-Reach 这个名字,很多人会以为它又是一个"套壳聊天机器人"。但如果你最近在折腾 AI Agent 开发,尤其是想让 Agent 真正去操作浏览器、点击按钮、填写表单、抓取页面数据…

作者头像 李华