从单体到协同:用 LangGraph 构建 Web3 投研的多智能体工作流

30 阅读5分钟

前言

在上一篇文章中,我们通过一个单体 Agent 实现了 Polymarket 与新闻数据的初步整合。然而,在面对波诡云谲的实战环境时,单体架构暴露出逻辑深度不足工具调用混乱以及无法自我纠错等硬伤。

为了解决这些痛点,本文将对原有的单智能体系统进行“手术级”重构,引入 LangGraph 实现多角色协同,让你的投研工具从一个“全栈练习生”进化为一支“专业特种部队”。

一、 单体 Agent 的“天花板”

在尝试构建 Web3 投研助手时,我们通常会给一个 Agent 塞进所有工具(搜索、行情、链上监测)。但在实战中,单体 Agent 常遇到三大痛点:

  1. 注意力涣散:Prompt 过长导致模型忽略了关键的风险提示。
  2. 逻辑闭环难:模型容易“自嗨”,拿到错误数据后直接开始推演,没有纠错机制。
  3. 工具冲突:当工具超过 5 个时,模型选择工具的准确率大幅下降。

二、 多智能体的降维打击:分工与制衡

多智能体架构的核心在于 “角色拆解” 。通过将任务分给不同的 Agent,我们模拟了一个专业投研机构的运作流水线:

1. 角色纯粹化(Specialization)

  • 研究员 (Researcher) :只负责“找”。它精通各种搜索语法,不带主观偏见地搬运事实。
  • 分析师 (Analyst) :只负责“想”。它不直接查数据,而是对研究员提供的情报进行逻辑建模、胜率计算和风险评估。

2. 动态博弈与纠错(Feedback Loop)

这是多智能体最强的地方:分析师可以“打回重做” 。如果研究员提供的情报不足以支撑结论,分析师会提出具体的补查要求,迫使系统进入循环直到逻辑闭环。

三、 实战:基于 LangGraph 的投研工作流

1.工具

import * as dotenv from "dotenv";
dotenv.config();
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { TavilySearch } from "@langchain/tavily";
import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";

const agent = new HttpsProxyAgent("http://127.0.0.1:3067");
const axiosConfig = { timeout: 15000, httpsAgent: agent, proxy: false };
const searchInstance = new TavilySearch({ maxResults: 5 }); // 增加搜索结果以捕捉更多套利新闻

// 1. 搜索工具:强化了对套利/异动新闻的搜索描述
export const financialSearchTool = tool(
    async (input) => {
        const query = typeof input === 'string' ? input : (input.query || JSON.stringify(input));
        console.log(`\n[🔍 正在执行深度搜索]: ${query}`);
        try {
            const res = await searchInstance.invoke(query);
            return JSON.stringify(res);
        } catch (e: any) { return `搜索暂时不可用`; }
    },
    {
        name: "financial_market_search",
        description: "搜索最新新闻背景、套利机会或市场异动。请输入关键词对象,例如:{\"query\": \"Polymarket arbitrage opportunities\"}",
        schema: z.object({ query: z.string() }), 
    }
);

// 2. 深度优化的行情工具 (Arbitrage & Trend Ready)
export const marketDataTool = tool(
    async (input) => {
        const userInput = typeof input === 'string' ? input : (input.marketName || JSON.stringify(input));
        
        // 1. 行业语义映射表 (包含最新套利关键词)
        const mapping: Record<string, string[]> = {
            "原油": ["oil", "crude", "energy", "brent", "wti", "gasoline"],
            "油价": ["oil", "crude", "energy"],
            "中东": ["israel", "gaza", "iran", "lebanon", "middle east", "hezbollah", "conflict"],
            "战争": ["war", "military", "strike", "attack", "invasion"],
            "选举": ["election", "trump", "vance", "harris", "walz"],
            "停火": ["ceasefire", "truce", "peace"],
            "核": ["nuclear", "facility", "isfahan"],
            "海峡": ["hormuz", "strait", "shipping"],
            "宏观": ["fed", "rate cut", "inflation", "recession", "gdp"],
            "套利": ["arbitrage", "mispricing", "spread", "basis", "hedging"],
            "时间差": ["deadline", "expiry", "until", "before", "sooner"],
            "美联储": ["powell", "fomc", "interest rate", "basis points"],
            "加密货币": ["bitcoin", "etf", "ethereum", "solana", "ath"]
        };

        // 获取基础关注词
        let baseTerms = [userInput.toLowerCase()];
        for (const [zh, ens] of Object.entries(mapping)) {
            if (userInput.includes(zh)) {
                baseTerms = ens;
                break;
            }
        }

        // --- 优化点:自动生成组合搜索词(捕获最新最热) ---
        const hotSuffixes = ["ceasefire", "rate cut", "ath", "trump", "deadline"];
        const focusTerms = [
            ...baseTerms,
            ...baseTerms.flatMap(term => hotSuffixes.map(suffix => `${term} ${suffix}`))
        ];

        console.log(`\n[📊 正在扫描套利机会]: 领域 -> ${userInput} | 衍生词数 -> ${focusTerms.length}`);

        try {
            // 获取全平台最火的 50 个市场
            const url = `https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=50`;
            const res = await axios.get(url, axiosConfig);
            
            if (!res.data || res.data.length === 0) return "Polymarket 暂无活跃市场。";

            // 2. 精准过滤逻辑
            const relevantMarkets = res.data.filter((m: any) => {
                const title = m.question.toLowerCase();
                // 必须包含正向词,剔除体育等干扰噪音
                const hasFocus = focusTerms.some(term => title.includes(term));
                const isNoise = ["nhl", "nba", "cup", "game", "soccer", "football"].some(noise => title.includes(noise));
                return hasFocus && !isNoise;
            });

            if (relevantMarkets.length > 0) {
                // 按相关度或题目排序,方便 Agent 对比相似市场寻找套利空间
                const marketList = relevantMarkets.map((m: any) => ({
                    question: m.question,
                    price: m.lastTradePrice,
                    endDate: m.endDate
                }));

                return JSON.stringify({
                    status: "success",
                    count: marketList.length,
                    data: marketList,
                    hint: "请分析以上市场之间是否存在隐含概率冲突或定价偏差。"
                });
            }

            return `[提示]:热门榜单中暂无直接对标 "${userInput}" 的套利交易对。`;
        } catch (e: any) { return `行情接口异常: ${e.message}`; }
    },
    {
        name: "get_realtime_market_data",
        description: "获取 Polymarket 实时赔率与套利机会。支持组合搜索。",
        schema: z.object({ marketName: z.string() }),
    }
);

export const tools = [financialSearchTool, marketDataTool];

2.主程

以下是使用 LangChain 最新 LangGraph 框架实现的协作代码片段:

import { ChatOpenAI } from "@langchain/openai";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { StateGraph, MessagesAnnotation, END } from "@langchain/langgraph";
import { financialSearchTool } from "./tools"; // 复用你之前的工具

// 1. 定义两个不同的 LLM 实例(可以给分析师更高的 Temperature 来激发灵感)
const llm = new ChatOpenAI({
  // 核心修复:显式指定 API Key 和 Base URL
  apiKey: process.env.DEEPSEEK_API_KEY, 
  modelName: "deepseek-chat",
  configuration: {
    baseURL: process.env.DEEPSEEK_API_BASE_URL,
  },
  temperature: 0,
});
// 2. 定义角色逻辑
// 研究员节点:强迫它必须使用工具
async function researcherNode(state: typeof MessagesAnnotation.State) {
  const systemMessage = {
    role: "system",
    content: "你是一名 Web3 研究员。你的任务是利用搜索工具获取关于特定事件的最新事实。获取事实后,直接将其传递给分析师,不要进行深度评论。",
  };
  const response = await llm.bindTools([financialSearchTool]).invoke([systemMessage, ...state.messages]);
  return { messages: [response] };
}

// 分析师节点:负责逻辑推演
async function analystNode(state: typeof MessagesAnnotation.State) {
  const systemMessage = {
    role: "system",
    content: "你是一名顶级高级分析师。你需要审查研究员提供的事实。如果事实太模糊,请要求研究员重新搜索;如果事实充足,请给出最终的套利分析报告。你的回复必须以 '【最终报告】' 开头。",
  };
  const response = await llm.invoke([systemMessage, ...state.messages]);
  return { messages: [response] };
}

// 3. 定义路由逻辑:判断是该继续搜,还是该结束了
function shouldContinue(state: typeof MessagesAnnotation.State) {
  const lastMessage = state.messages[state.messages.length - 1];
  // 如果分析师说了“最终报告”,就结束
  if (typeof lastMessage.content === "string" && lastMessage.content.includes("【最终报告】")) {
    return END;
  }
  // 如果有工具调用,去执行工具
  if (lastMessage.additional_kwargs.tool_calls) {
    return "tools";
  }
  // 否则,让分析师看研究员的结果
  return "analyst";
}

// 4. 构建工作流图 (Graph)
const workflow = new StateGraph(MessagesAnnotation)
  .addNode("researcher", researcherNode)
  .addNode("analyst", analystNode)
  .addNode("tools", new ToolNode([financialSearchTool])) // 专门执行工具的节点
  
  // 连线逻辑
  .addEdge("__start__", "researcher") // 从研究员开始
  .addEdge("tools", "researcher")      // 工具执行完后回到研究员
  .addConditionalEdges("researcher", shouldContinue) // 研究员做完后判断:调工具还是给分析师
  .addConditionalEdges("analyst", shouldContinue);    // 分析师做完后判断:结束还是打回重搜

// 5. 编译并运行
const app = workflow.compile();

async function runMultiAgent() {
  const inputs = { messages: [{ role: "user", content: "分析以伊冲突对 Polymarket 原油预测价格的影响" }] };
  const result = await app.invoke(inputs);
  
  console.log("\n--- 协作过程结束 ---");
  console.log(result.messages[result.messages.length - 1].content);
}

runMultiAgent();

为什么这套代码能跑赢单体模型?

  • 状态可控:你可以清晰看到请求是在“搜索”阶段还是“审计”阶段。
  • 递归深度:通过 recursionLimit 限制,你可以防止 Agent 陷入死循环,同时保证了深度。
  • 模型异构:你可以让研究员用便宜快速的 GPT-4o-mini,而让分析师用逻辑更强的 DeepSeek-V3 或 Claude 3.5。

结语

单体 AI 是工具,多智能体才是“数字员工”。在信息密度极高的 Web3 领域,学会如何编排一群 Agent 协作,将是开发者真正的竞争壁垒。