前端使用langgraph.js开发一个对话智能体,含工具调用 联网搜索
只是写来练手,不建议直接在前端直接调用大模型接口,会暴露apikey。一个简单的智能体,联网搜索使用serper,需要自行申请api。
·
一个简单的智能体,联网搜索使用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}`)
}
}
}
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)