AI Research Agent 自动研究分析:用 Openclaw 思路搭一套可复现的 RAG 研究流
发布时间:2026/10/1 14:59:42来源:尧图网络
1. 从一次竞品调研说起AI Research Agent 到底解决什么问题你可能遇到过这种场景老板丢来一句“把市面上主流 AI 编程助手做个对比明天给我”然后你打开十几个网页复制粘贴、整理表格、反复核对一整天就没了。AI Research Agent 要做的就是把这条链路自动化——你只提一个问题Agent 自己去检索、阅读、归纳、追问最后吐出一份带引用来源的结构化报告。它和普通聊天机器人的区别在于聊天机器人是“你问一句它答一句”而 Research Agent 是“你给一个目标它自己规划步骤并执行”。核心能力拆成四块就是 Planning拆任务、Tool调搜索/爬虫、Memory记住读过什么、RAG把长文切块检索后再归纳。Openclaw 这类项目的思路之所以值得借鉴是因为它把这几块用很轻的方式串了起来没有过度工程化一个前端工程师花两三天就能跑通最小闭环。适合谁跟做有基础 JavaScript/TypeScript 能力、想理解 Agent 编排本质、又不想一上来就啃重型框架的人。我试过用 LangChain 全家桶起步结果光理解抽象层就耗掉半天后来换成“手写 Planner 工具函数”的 Openclaw 式思路反而更快看到结果。下面这套流程从提问到结构化报告每一步都能复制。整个系统的数据流是这样的用户问题进入 PlannerPlanner 产出一个步骤列表搜索→阅读→归纳→成文Executor 按步骤调用工具Search Tool 拿回候选链接Web Reader 抓正文LLM 做分段摘要最后 Report Generator 汇总成报告。中间所有抓取内容进向量库做 RAG 检索多轮追问时从 Memory 里捞上下文。听起来模块多但每个模块代码量都不大关键是接口对齐。2. TaoToken 前置给 Agent 接一个稳定的模型入口Agent 跑起来最怕的不是逻辑写错而是模型调用不稳定——搜索到一半 401摘要生成到一半超时整个流程就断了。所以在写 Planner 之前先把模型入口配好。这里用 TaoToken 作为统一入口它兼容 OpenAI 风格的接口改一个 Base URL 就能切换模型对 Research Agent 这种需要频繁调 LLM 的场景很友好。你需要准备三样东西Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面生成Model ID 按你实际要用的模型填比如做归纳用推理能力强的做摘要用速度快的。这三件套在后面的 JSON 配置、环境变量、代码里会反复出现先记牢。为什么 Agent 场景特别需要统一入口因为 Research Agent 一次任务里会调用 LLM 很多次Planner 拆步骤一次、每个网页分段摘要 N 次、最终成文一次、多轮追问再来几次。如果每次都要换不同的 SDK 和鉴权方式代码会变得很脏。统一成 OpenAI 兼容格式后你只需要维护一个 client 实例换模型只改 Model ID 字符串。配置方式有两种选一种就行。第一种是环境变量适合本地开发export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_MODEL你的模型ID第二种是写进项目的 settings 文件适合团队协作时统一。以 Node 项目为例建一个config/agent.settings.json{ llm: { baseURL: https://taotoken.net/api, apiKey: sk-你的key, model: 你的模型ID, temperature: 0.3, maxTokens: 2048 }, agent: { maxSteps: 5, timeoutMs: 60000, topK: 5 } }注意temperature设低一点0.2~0.4研究类任务要的是稳定归纳而不是发散创作。maxSteps和timeoutMs是防止 Agent 无限循环的保险丝后面排障会讲。API Key 不要硬编码进提交到仓库的文件用.env或密钥管理这里为了演示直观才写进 JSON。配好之后写一个最小验证脚本确认模型入口通了再往下做import OpenAI from openai; import settings from ./config/agent.settings.json assert { type: json }; const client new OpenAI({ baseURL: settings.llm.baseURL, apiKey: settings.llm.apiKey, }); const res await client.chat.completions.create({ model: settings.llm.model, messages: [{ role: user, content: 回复两个字就绪 }], }); console.log(res.choices[0].message.content);跑出“就绪”两个字说明 Base URL、Key、Model ID 三件套都对。这一步别跳过后面所有报错排查都以这个脚本为基准。3. 可复制配置Planner、工具与 RAG 检索的完整片段这一节是核心把 Agent 的骨架搭出来。我按 Openclaw 的思路拆成四个文件planner.ts负责拆任务tools/search.ts和tools/crawler.ts负责取数据memory/vectorStore.ts负责 RAG 检索executor.ts负责串流程。先看 Planner 的提示词模板这是整个 Agent 的大脑。// backend/agent/prompts.ts export const PLANNER_PROMPT 你是一个研究规划器。用户会给你一个研究问题你需要输出一个 JSON 步骤数组。 可用步骤类型只有四种 - search: 生成搜索关键词 - read: 阅读指定 URL 的正文 - summarize: 对已读内容做归纳 - report: 生成最终报告 规则 1. 最多 5 步不要输出解释性文字只输出 JSON。 2. 第一步必须是 search。 3. 最后一步必须是 report。 4. 每一步带一个 input 字段说明具体做什么。 输出格式示例 [ {step: search, input: AI Agent 发展趋势 2025}, {step: read, input: 从搜索结果中选前3条}, {step: summarize, input: 归纳核心观点}, {step: report, input: 生成结构化报告} ] ;Planner 的输出必须是严格 JSON否则 Executor 解析会崩。实测下来把“只输出 JSON”写进提示词还不够最好在代码里加一层容错用正则把第一个[到最后一个]之间的内容抠出来再JSON.parse。这样即使模型多说了两句废话也不影响流程。接下来是 Search Tool。这里用 Serper 做演示你也可以换成任何搜索 API接口结构类似// backend/tools/search.ts export async function search(query: string) { const res await fetch(https://google.serper.dev/search, { method: POST, headers: { X-API-KEY: process.env.SERPER_KEY!, Content-Type: application/json, }, body: JSON.stringify({ q: query, num: 8 }), }); const data await res.json(); return data.organic.slice(0, 5).map((item: any) ({ title: item.title, link: item.link, snippet: item.snippet, })); }Web Reader 用 Cheerio 抓正文。这里有个坑直接$(body).text()会把导航栏、页脚、广告全抓进来噪声极大。更好的做法是优先取article、main标签取不到再退回 body// backend/tools/crawler.ts import * as cheerio from cheerio; export async function readWeb(url: string) { const html await fetch(url, { headers: { User-Agent: Mozilla/5.0 (compatible; ResearchAgent/1.0) }, }).then((r) r.text()); const $ cheerio.load(html); $(script, style, nav, footer, aside).remove(); const main $(article).text() || $(main).text() || $(body).text(); return main.replace(/\s/g, ).trim().slice(0, 12000); }RAG 检索这块Openclaw 式思路不追求上重型向量库先用内存数组 余弦相似度就能跑通。把每个网页按 800 字切块调 embedding 接口存起来追问时按相似度取 topK// backend/memory/vectorStore.ts type Chunk { text: string; source: string; embedding: number[] }; const store: Chunk[] []; export function chunkText(text: string, size 800): string[] { const chunks: string[] []; for (let i 0; i text.length; i size) { chunks.push(text.slice(i, i size)); } return chunks; } export function cosine(a: number[], b: number[]) { let dot 0, na 0, nb 0; for (let i 0; i a.length; i) { dot a[i] * b[i]; na a[i] * a[i]; nb b[i] * b[i]; } return dot / (Math.sqrt(na) * Math.sqrt(nb)); } export function retrieve(queryEmb: number[], topK 5) { return store .map((c) ({ ...c, score: cosine(queryEmb, c.embedding) })) .sort((a, b) b.score - a.score) .slice(0, topK); }Executor 把上面这些串起来核心是一个 for 循环加 switch// backend/agent/executor.ts export async function execute(question: string) { const plan await planSteps(question); const context: string[] []; for (const step of plan) { if (step.step search) { const results await search(step.input); context.push(...results.map((r) ${r.title} | ${r.link})); } else if (step.step read) { const urls extractUrls(context); for (const url of urls.slice(0, 3)) { const text await readWeb(url); await indexChunks(text, url); } } else if (step.step summarize) { const summary await summarizeWithLLM(context.join(\n)); context.push(summary); } else if (step.step report) { return await generateReport(question, context); } } }indexChunks负责调 embedding 接口并写入 storesummarizeWithLLM和generateReport都是标准的 chat.completions 调用用第 2 节配好的 client 即可。报告生成的提示词要强制引用来源这是防幻觉的关键export const REPORT_PROMPT 基于以下资料生成研究报告必须包含标题、摘要、核心观点、趋势分析、参考来源。 要求每个核心观点后标注来源编号 [1][2]只允许使用资料中出现的信息不得编造。 资料 {context} ;4. 验证请求从提问到结构化报告的完整跑通配置写完跑一次完整验证。用一个真实问题“对比主流 AI 编程助手的核心差异”。启动流程后观察控制台输出正常应该看到这样的执行轨迹[Planner] 生成 4 步计划 [Search] 关键词: AI 编程助手 对比 2025 [Search] 返回 5 条结果 [Read] 抓取 3 个网页共 28400 字 [Index] 切分 36 个 chunk写入向量库 [Summarize] 归纳出 6 个核心观点 [Report] 生成报告引用来源 5 条最终报告的结构化输出大致长这样# AI 编程助手核心差异对比 ## 摘要 主流工具在补全准确率、上下文长度、Agent 能力三个维度分化明显... ## 核心观点 1. 补全类工具侧重低延迟Agent 类工具侧重多步任务 [1][3] 2. 上下文窗口从 128K 向 200K 演进 [2] 3. 本地部署与云端调用的取舍取决于数据合规要求 [4] ## 趋势分析 多 Agent 协作与工具调用标准化是下一阶段重点 [5] ## 参考来源 [1] https://... [2] https://...验证成功的三个标志一是报告里每个观点都有来源编号二是来源链接能点开且内容相关三是整个流程耗时在 60 秒内。如果报告里出现没有编号的“裸观点”说明提示词约束不够回去把REPORT_PROMPT里的“不得编造”再强调一遍并在代码里做后处理——扫描报告中的句子没有[n]标记的段落打回重写。多轮追问的验证也要做。在报告生成后追加一句“第二个观点有数据支撑吗”Agent 应该走 RAG 检索从向量库里捞出相关 chunk再调 LLM 回答。这一步验证的是 Memory 模块是否真的在工作。如果追问时 Agent 答非所问多半是 embedding 没存进去或者检索时 query 没做 embedding检查retrieve调用前有没有先算 query 向量。5. 本篇常见错排查401、local proxy failed 与 reading choices跑 Agent 流程时报错集中在几个地方我按出现频率排一下。401 Unauthorized最常见。先确认TAOTOKEN_API_KEY有没有正确加载Node 里process.env读不到多半是没装dotenv或没在入口import dotenv/config。其次检查 Base URL 有没有多写斜杠https://taotoken.net/api后面不要再加/v1OpenAI SDK 会自己拼路径。如果用的是 settings.json 方式确认 JSON 里没有尾逗号JSON 解析失败会静默变成 undefined。local proxy failed / connection refused这个报错通常不是模型入口的问题而是你的爬虫在抓某个网页时被目标站拒绝或者本地网络策略拦截。排查方法把readWeb单独拿出来用固定 URL 测一次看是 fetch 阶段挂还是 cheerio 解析阶段挂。如果是 fetch 挂加超时和重试const controller new AbortController(); const timer setTimeout(() controller.abort(), 10000); const html await fetch(url, { signal: controller.signal }).then(r r.text()); clearTimeout(timer);reading choices 报错Cannot read properties of undefined (reading choices)说明 LLM 返回体结构不对res.choices是 undefined。三种可能一是 API 返回了错误对象比如额度不足、模型名写错你没检查res.error就直接取 choices二是流式和非流式混用三是 Model ID 填错接口返回了非预期结构。加一层防御const res await client.chat.completions.create({...}); if (!res.choices || !res.choices[0]) { console.error(LLM 返回异常:, JSON.stringify(res)); throw new Error(模型调用失败检查 Model ID 与额度); }OAuth / 鉴权相关报错如果你用的是 Claude Code 或 Codex 这类带 OAuth 的工具链报错信息里出现 token 过期注意区分“工具自身的登录态”和“模型 API 的 Key”。Research Agent 走的是 API Key 鉴权不涉及 OAuth 流程。如果你在 CC Switch 或 Cline MCP 里配置三件套要写全Base URL 填https://taotoken.net/apiKey 填生成的 sk- 开头字符串Model ID 填具体模型名缺一个都会鉴权失败。Agent 无限循环表现为控制台一直刷[Search]。这是 Planner 没约束好或者 Executor 的maxSteps没生效。在 Executor 循环里加计数器超过阈值强制跳到 report 步骤。同时给整个 execute 包一层Promise.race做超时await Promise.race([ execute(question), new Promise((_, reject) setTimeout(() reject(new Error(Agent 超时)), 60000)), ]);报告全是幻觉来源编号对不上或者引用了不存在的链接。根因是 summarize 阶段把多个网页内容混在一起喂给 LLM模型分不清哪句来自哪个源。解决方法是分段摘要时保留 source 元数据最终成文时按 source 分组传入并在提示词里明确“来源 [n] 对应 URL 列表如下”。6. 语义一致 CTA把这条研究流接到你的工作里跑通最小闭环后你会发现这套结构的扩展性比想象中好。想加深度研究模式就在 Planner 里允许search → read → search again的循环想加多 Agent 协作就把 summarize 拆成独立的 Researcher Agent 和 Writer Agent用消息队列串起来想加前端可视化把 Executor 每步的日志通过 SSE 推给 Next.js 页面就能看到“正在搜索”“正在阅读”的实时状态。模型入口这块如果你要长期跑研究类 Agent调用量会比普通对话大不少建议在控制台里把用量监控开起来按任务维度打标签方便定位是哪个环节在烧 token。需要生成新的 API Key 或查看额度去控制台的 API Keys 页面操作想先验证模型对话效果再接入代码可以用模型对话页面直接测提示词如果打算把这条流做成长期跑的编码/研究 AgentCoding Plan 里有更完整的额度方案。接入文档里有 OpenAI 兼容接口的完整参数说明包括流式、函数调用、embedding 的用法写 RAG 检索那部分时对照着看能少踩坑。整套流程的代码结构不复杂难的是把每个环节的边界条件处理好——超时、重试、幻觉约束、循环上限这些才是 Research Agent 从 demo 到能用的分水岭。
网站建设高端定制企业官网