一个简单的智能体,联网搜索使用serper,需要自行申请api

只是写来练手,不建议直接在前端直接调用大模型接口,会暴露apikey

依赖:

    "@langchain/anthropic": "^0.0.0",
    "@langchain/core": "^1.0.5",
    "@langchain/langgraph": "^1.0.2",
    "@langchain/openai": "^1.1.2",

主页面:

<!-- LLMWithTools.vue -->
<template>
  <div>
    <h1>LangGraphjs with Tools</h1>
    <input
      v-model="userInput"
      placeholder="Ask a question..."
      @keyup.enter="runGraph"
    />
    <button @click="runGraph" :disabled="isLoading">
      {{ isLoading ? 'Thinking...' : 'Send' }}
    </button>
    <div v-if="finalResult">
      <h2>Result:</h2>
      <pre>{{ finalResult }}</pre>
    </div>
    <div v-if="error">
      <h2>Error:</h2>
      <pre style="color: red">{{ error }}</pre>
    </div>
  </div>
</template>

<script>
import { ChatOpenAI } from '@langchain/openai'
import { HumanMessage, AIMessage, ToolMessage } from '@langchain/core/messages'
import { StructuredTool } from '@langchain/core/tools'
import { StateGraph, END, START } from '@langchain/langgraph/web'
import { z } from 'zod'

export default {
  data() {
    return {
      userInput: "深圳的天气是?",
      finalResult: null,
      error: null,
      isLoading: false,
      app: null,
    }
  },
  created() {
    this.initializeGraph()
  },
  methods: {
    async initializeGraph() {
      try {
        const { ToolManager } = await import('@/services/toolManager')
        const toolsJson = await ToolManager.getToolDefinitions()
        const model = new ChatOpenAI({
          modelName: 'deepseek-chat',
          apiKey: '',
          configuration: {
            baseURL: 'https://api.deepseek.com/v1',
          },
        }).bindTools(toolsJson)

        // --- 定义 agent 节点 ---
        const agent = async (state) => {
          const messages = state.messages || []
          const response = await model.invoke(messages)
          return { messages: [response] }
        }

        // --- 定义 action 节点(执行工具) ---
        const action = async (state) => {
          console.log('state---->', state)
          const messages = state.messages || []
          const lastMessage = messages[messages.length - 1]
          console.log('lastMessage----->', lastMessage)
          if (
            !lastMessage ||
            !lastMessage.tool_calls ||
            lastMessage.tool_calls.length === 0
          ) {
            // 没有工具调用,直接返回空
            return { messages: [] }
          }
          const toolMessages = await Promise.all(
            lastMessage.tool_calls.map(async (toolCall) => {
              const toolName = toolCall.name || toolCall.tool_name || toolCall.tool
              let toolArgs = toolCall.arguments ?? toolCall.args ?? toolCall.input ?? null
              const result = await ToolManager.executeTool(toolName, toolArgs)
              console.log('result------->', result)
              return new ToolMessage({
                content: result.results?JSON.stringify(result.results):"没有执行结果",
                tool_call_id: toolCall.id, // 必须回传ID
                name: toolName,
              })
            })
          )
          console.log("toolMessages------->",toolMessages)
          return {messages:toolMessages}
        }

        // --- 条件函数:agent -> action OR 结束 ---
        const shouldContinue = (state) => {
          const messages = state.messages || []
          const lastMessage = messages[messages.length - 1]
          console.log('lastMessage------->', lastMessage)
          if (
            !lastMessage ||
            !lastMessage.tool_calls ||
            lastMessage.tool_calls.length === 0
          ) {
            return END
          }
          return 'action'
        }

        // --- 构建 StateGraph ---
        const agentState = {
          messages: {
            value: (x, y) =>
              Array.isArray(x) ? x.concat(y) : Array.isArray(y) ? y : [y],
            default: () => [],
          },
        }

        const workflow = new StateGraph({ channels: agentState })
        workflow.addNode('agent', agent)
        workflow.addNode('action', action)
        workflow.addEdge(START, 'agent')
        workflow.addConditionalEdges('agent', shouldContinue)
        workflow.addEdge('action', 'agent')

        this.app = workflow.compile()

        console.log('Graph initialized (robust version).')
      } catch (e) {
        this.error = `Graph initialization failed: ${e.message || String(e)}`
        console.error(e)
      }
    },

    extractFinalText(finalState) {
      if (
        !finalState ||
        !finalState.messages ||
        finalState.messages.length === 0
      )
        return null
      for (let i = finalState.messages.length - 1; i >= 0; i--) {
        const m = finalState.messages[i]
        if (!m) continue
        if (typeof m.content === 'string' && m.content.trim()) return m.content
        if (m.output && typeof m.output === 'string' && m.output.trim())
          return m.output
        if (m.content && typeof m.content === 'object') {
          if (m.content.text) return m.content.text
          if (m.content.response) return m.content.response
        }
      }
      return null
    },

    async runGraph() {
      this.error = null
      this.finalResult = null

      if (!this.userInput || !this.userInput.trim()) {
        this.error = 'Please enter a question.'
        return
      }
      if (!this.app) {
        this.error = 'Graph is not initialized.'
        return
      }

      this.isLoading = true
      try {
        const inputs = {
          messages: [new HumanMessage(this.userInput)],
        }
        console.log('inputs---->', inputs)
        const finalState = await this.app.invoke(inputs)
        console.log('finalState------->', finalState)
        const answer = this.extractFinalText(finalState)
        if (answer) {
          this.finalResult = answer
        } else {
          this.error =
            'Could not extract final answer. See console for finalState.'
          console.warn('finalState returned:', finalState)
        }
      } catch (e) {
        console.error('Error invoking graph:', e)
        this.error = `Error during graph execution: ${e.message || String(e)}`
      } finally {
        this.isLoading = false
      }
    },
  },
}
</script>

<style scoped>
input {
  padding: 8px;
  margin-right: 10px;
  width: 300px;
  border: 1px solid #ccc;
  border-radius: 4px;
}
button {
  padding: 8px 15px;
  border: none;
  background-color: #42b983;
  color: white;
  border-radius: 4px;
  cursor: pointer;
}
button:disabled {
  background-color: #ccc;
}
pre {
  margin-top: 15px;
  background-color: #f3f3f3;
  padding: 15px;
  border-radius: 4px;
  white-space: pre-wrap;
  word-wrap: break-word;
  font-family: monospace;
}
</style>

工具封装toolManager.js:

import { WebSearchTool } from '@/tools/WebSearchTool'
import { URLFetcherTool } from '@/tools/URLFetcherTool'
import { TaskTool } from '@/tools/TaskTool'
// import { CalculatorTool } from '@/tools/CalculatorTool'

export class ToolManager {
  static tools = {
    'WebSearch': WebSearchTool,
    'URLFetcher': URLFetcherTool,
    'TaskTool': TaskTool,
    // 'Calculator': CalculatorTool
  }

  static async executeTool(toolName, input) {
    const tool = this.tools[toolName]
    
    if (!tool) {
      throw new Error(`工具不存在: ${toolName}`)
    }
    console.log("tool---->",tool)
    try {
      // 验证输入
      if (tool.inputSchema) {
        // 简单的输入验证
        const required = tool.inputSchema.required || []
        for (const field of required) {
          if (!input[field]) {
            throw new Error(`缺少必要参数: ${field}`)
          }
        }
      }

      // 执行工具
      const result = await tool.execute(input)
      return result
    } catch (error) {
      console.log("error----222")
      console.error(`工具执行失败 ${toolName}:`, error)
      throw new Error(`工具执行失败: ${error.message}`)
    }
  }

  static getAvailableTools() {
    return Object.keys(this.tools).map(name => ({
      name,
      description: this.tools[name].description,
      inputSchema: this.tools[name].inputSchema
    }))
  }

  static async getToolDescription(toolName) {
    const tool = this.tools[toolName]
    if (!tool) {
      throw new Error(`工具不存在: ${toolName}`)
    }
    
    if (typeof tool.description === 'function') {
      return await tool.description()
    }
    
    return tool.description || '无描述'
  }

  // 获取工具定义,用于传递给AI模型
  static async getToolDefinitions() {
    const toolDefinitions = []
    
    for (const [name, tool] of Object.entries(this.tools)) {
      const description = await this.getToolDescription(name)
      
      const toolDefinition = {
        type: 'function',
        function: {
          name: name,
          description: description,
          parameters: tool.inputSchema || {
            type: 'object',
            properties: {},
            required: []
          }
        }
      }
      
      toolDefinitions.push(toolDefinition)
    }
    
    return toolDefinitions
  }
}

联网搜索工具:

import axios from 'axios'

export const WebSearchTool = {
  name: 'WebSearch',
  description: '在互联网上搜索信息,提供当前事件和最近数据的最新信息',
  inputSchema: {
    type: 'object',
    properties: {
      query: {
        type: 'string',
        description: '搜索关键词'
      },
      maxResults: {
        type: 'number',
        description: '最大结果数量',
        default: 5
      },
      searchEngine: {
        type: 'string',
        description: '搜索引擎类型',
        enum: ['bing', 'duckduckgo', 'serper'],
        default: 'serper'
      }
    },
    required: ['query']
  },

  async execute(input) {
    const { query, maxResults = 5, searchEngine = 'serper' } = input

    try {
      const searchResults = await this.performSearch(query, maxResults, searchEngine)

      return {
        success: true,
        query,
        searchEngine,
        results: searchResults,
        summary: `使用${searchEngine}搜索"${query}",找到 ${searchResults.length} 个相关结果`
      }
    } catch (error) {
      return {
        success: false,
        error: error.message,
        query,
        searchEngine
      }
    }
  },

  async performSearch(query, maxResults, searchEngine) {
    switch (searchEngine) {
      case 'serper':
      default:
        return await this.searchDuckDuckGo(query, maxResults)
    }
  },

  // DuckDuckGo Instant Answer API
  async searchDuckDuckGo(query, maxResults) {
    try {
      // const response = await axios.get('https://api.duckduckgo.com/', {
      //   params: {
      //     q: query,
      //     format: 'json',
      //     no_html: 1,
      //     skip_disambig: 1
      //   },
      //   timeout: 10000
      // })
      const myHeaders = new Headers();
      // 自行申请APIKEY
      myHeaders.append("X-API-KEY", "");
      myHeaders.append("Content-Type", "application/json");

      const raw = JSON.stringify({
        "q":query,
        "num":maxResults
      });

      const requestOptions = {
        method: "POST",
        headers: myHeaders,
        body: raw,
        redirect: "follow"
      };

      try {
        const response = await fetch("https://google.serper.dev/search", requestOptions);
        const data = await response.text();
        const result = JSON.parse(data)
        const results = []
        if (result.organic && result.organic.length > 0) {
          result.organic.forEach((topic, index) => {
              results.push({
                title: topic.title || `结果 ${index + 1}`,
                // url: topic.link,
                snippet: topic.snippet,
                source: 'serper'
              })
          })
        }

        return results&&results.length > 0 ? results : [{
          title: '未找到相关结果',
          url: '',
          snippet: `没有找到关于"${query}"的搜索结果`,
          source: 'DuckDuckGo'
        }]

      } catch (error) {
        console.error(error);
      };

      // const results = []
      // console.log("response------------>>>",response)
      // // 提取相关主题
      // if (response.data.RelatedTopics && response.data.RelatedTopics.length > 0) {
      //   response.data.RelatedTopics.slice(0, maxResults).forEach((topic, index) => {
      //     if (topic.Text && topic.FirstURL) {
      //       results.push({
      //         title: topic.Text.split(' - ')[0] || `结果 ${index + 1}`,
      //         url: topic.FirstURL,
      //         snippet: topic.Text,
      //         source: 'DuckDuckGo'
      //       })
      //     }
      //   })
      // }

      // 如果没有相关主题,返回摘要
      // if (results.length === 0 && result.data.Abstract) {
      //   results.push({
      //     title: response.data.Heading || '摘要',
      //     url: response.data.AbstractURL || '',
      //     snippet: response.data.Abstract,
      //     source: 'DuckDuckGo'
      //   })
      // }
    } catch (error) {
      throw new Error(`搜索失败: ${error.message}`)
    }
  }
}
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐