1. 从零开始:为什么我们需要一个“最小版本”的Cursor?
如果你最近在关注AI编程助手,或者尝试过用LangChain、LangGraph这类框架来构建自己的AI应用,那你大概率听说过Cursor。它不仅仅是一个编辑器,更像是一个集成了强大AI能力的编程副驾驶。但很多时候,我们需要的可能不是那个功能庞杂、界面繁复的完整版Cursor,而是一个能嵌入到自己项目里的、轻量级的“大脑”——一个能理解代码、调用工具、并执行任务的AI Agent核心。
这就是“手写Cursor最小版本”这个项目的出发点。它不是一个简单的代码补全工具,而是一个具备自主思考和行动能力的“工具调用(Tool Calling)”引擎。想象一下,你有一个Node.js后端服务,用户输入一句自然语言,比如“帮我创建一个用户注册的API接口”,这个引擎就能自动分析需求,调用预设的代码生成工具,并最终输出可运行的代码片段。这背后,就是Agent(智能体)和Tool(工具)的核心思想。
市面上成熟的框架,比如LangChain,已经提供了非常完善的Agent构建能力。但直接使用这些框架,有时会感觉像在开一辆自动驾驶的豪华轿车——你得到了便利,却可能对引擎盖下的工作原理一无所知。当出现“deepseek returned tool calls without replayable thinking content”这类底层错误时,或者当你需要深度定制工具调用逻辑时,这种“黑盒”感会让人非常无力。
因此,手动实现一个最小化的Cursor核心,其价值在于深度理解。通过从零搭建,你将彻底搞懂几个关键问题:Agent是如何做决策的?Tool的描述(description)和参数(parameters)是如何被大模型理解和解析的?一次完整的“思考-行动-观察”循环(ReAct模式)是如何流转的?理解了这些,你不仅能更好地使用LangChain等高级框架,更能打造出完全贴合自己业务需求的、高性能、可解释的AI Agent。
2. 核心架构拆解:一个最小Agent系统需要哪些部件?
一个能工作的最小Agent系统,远不止是调用一下大模型的Chat Completion接口那么简单。它需要一套精密的协作机制。我们可以将其核心抽象为四个部分:大脑(LLM)、工具库(Tool Registry)、执行引擎(Agent Executor)和记忆与状态(Memory/State)。我们的“最小版本”将聚焦前三个,实现最基础的闭环。
2.1 大脑:与大模型对话的接口
这是系统的智能核心。我们选择Node.js环境,因为它异步非阻塞的特性非常适合处理AI API调用这类I/O密集型任务。我们将使用OpenAI格式兼容的API(例如DeepSeek、GPT等)作为我们的大脑。
首先,我们需要一个可靠的客户端。这里不直接使用OpenAI官方SDK,是为了保持灵活性,方便切换不同的模型提供商。
// llmClient.js import fetch from 'node-fetch'; class LLMClient { constructor(apiKey, baseURL = 'https://api.deepseek.com') { this.apiKey = apiKey; this.baseURL = baseURL; } async chatCompletion(messages, model = 'deepseek-chat', temperature = 0.1) { // temperature调低,使模型输出更稳定,更适合工具调用 const url = `${this.baseURL}/chat/completions`; const body = { model, messages, temperature, // 关键:请求模型以JSON格式返回,并支持工具调用 response_format: { type: "json_object" }, // 在实际最小版本中,我们可能先不启用streaming stream: false, }; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, }, body: JSON.stringify(body), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`LLM API Error: ${response.status} ${errorText}`); } const data = await response.json(); return data.choices[0].message; } } export default LLMClient;为什么这么设计?
- 封装性:将API调用细节隐藏起来,后续如果要增加重试、日志、缓存等功能,只需修改这一个类。
- 参数预设:将
temperature默认设为0.1,是因为工具调用需要模型输出结构化的内容(如JSON),低随机性可以提高成功率。response_format提示模型返回JSON,为我们解析工具调用指令铺平道路。 - 错误处理:对非200的HTTP状态码进行统一处理,抛出包含详细信息的错误,便于上游排查。这是避免后期调试时面对“
error installing 24.19.0: node.js v24.19.0 is not yet released”这种模糊报错的好习惯。
2.2 工具库:定义Agent的“手脚”
Tool是Agent能力的延伸。每个Tool都必须有清晰的定义,包括名称、描述和参数模式。大模型正是根据这些描述来决定何时以及如何使用它们的。
// toolRegistry.js class Tool { constructor(name, description, parameters, executeFunc) { this.name = name; this.description = description; // 给模型看的“说明书” this.parameters = parameters; // JSON Schema格式的参数定义 this._execute = executeFunc; // 实际的执行函数 } async execute(args) { // 这里可以加入参数验证、日志、性能监控等 console.log(`[Tool ${this.name}] 执行,参数:`, JSON.stringify(args)); try { const result = await this._execute(args); return result; } catch (error) { return `工具执行失败: ${error.message}`; } } // 将工具定义转换为模型能理解的格式(OpenAI Tool格式) toOpenAITool() { return { type: 'function', function: { name: this.name, description: this.description, parameters: this.parameters, }, }; } } class ToolRegistry { constructor() { this.tools = new Map(); } register(tool) { if (this.tools.has(tool.name)) { throw new Error(`工具名称 "${tool.name}" 已存在`); } this.tools.set(tool.name, tool); } getTool(name) { const tool = this.tools.get(name); if (!tool) { throw new Error(`未找到工具: "${name}"`); } return tool; } getAllTools() { return Array.from(this.tools.values()); } // 获取所有工具的OpenAI格式定义 getOpenAITools() { return this.getAllTools().map(tool => tool.toOpenAITool()); } } // 示例:创建一个简单的代码查询工具 const searchCodeTool = new Tool( 'search_code', '根据关键词在项目代码库中搜索相关的函数、类或代码片段。', { type: 'object', properties: { keyword: { type: 'string', description: '搜索关键词,如函数名、类名或功能描述', }, file_extension: { type: 'string', description: '文件扩展名过滤,如 .js, .py,默认为空', default: '', }, }, required: ['keyword'], }, async ({ keyword, file_extension }) => { // 这里是一个模拟实现。真实场景下,可以集成ripgrep、fzf等 // 或者连接本地的代码索引数据库。 const mockResults = [ `文件: utils.js, 行: 15-30\nfunction calculateDiscount(price, rate) { ... }`, `文件: models/User.js, 行: 5-10\nclass User { constructor(name) { ... } }`, ]; return `找到 ${mockResults.length} 个结果:\n` + mockResults.join('\n---\n'); } ); export { Tool, ToolRegistry, searchCodeTool };核心细节与避坑点:
- 描述(description)是灵魂:模型的工具调用完全基于描述。描述必须清晰、无歧义,说明工具的用途和适用场景。例如,“搜索代码”就不如“根据关键词在项目代码库中搜索相关的函数、类或代码片段”来得精确。
- 参数模式(parameters)要严谨:必须使用JSON Schema格式。
required字段至关重要,它告诉模型哪些参数是必须提供的。参数描述也要详细,这能极大提高模型填充参数的准确性。 - 执行函数的健壮性:
execute方法内部的try...catch是必须的。一个工具的失败不应导致整个Agent崩溃,而应将错误信息作为“观察”返回给大脑,让它决定下一步怎么做(比如重试或换一种方式)。 - 工具格式转换:
toOpenAITool()方法是为了适配OpenAI的API格式。这是与LLM通信的“协议”,必须严格遵守。
2.3 执行引擎:驱动“思考-行动”循环
这是粘合大脑和工具的核心调度器。它将实现经典的ReAct(Reasoning + Acting)模式:接收用户问题 -> 模型思考并决定调用工具 -> 执行工具 -> 将结果作为观察返回给模型 -> 模型进行下一轮思考或给出最终答案。
// agentExecutor.js import LLMClient from './llmClient.js'; import { ToolRegistry } from './toolRegistry.js'; class AgentExecutor { constructor(llmClient, toolRegistry, maxIterations = 5) { this.llm = llmClient; this.toolRegistry = toolRegistry; this.maxIterations = maxIterations; // 防止无限循环 } async run(userInput, systemPrompt = null) { // 初始化对话历史 const messages = []; if (systemPrompt) { messages.push({ role: 'system', content: systemPrompt }); } messages.push({ role: 'user', content: userInput }); let finalAnswer = null; let iteration = 0; // 开始ReAct循环 while (iteration < this.maxIterations && finalAnswer === null) { iteration++; console.log(`\n=== 第 ${iteration} 轮迭代 ===`); // 1. 调用大脑进行“思考” // 将可用工具的定义随请求发送给模型 const tools = this.toolRegistry.getOpenAITools(); const requestMessages = [...messages]; // 如果是第一次之后的迭代,需要把上一次的工具调用和结果也加入历史 // 这里简化处理,实际更复杂的Agent会维护完整的交互历史 const llmResponse = await this.llm.chatCompletion(requestMessages); // 2. 解析模型的响应 // 理想情况下,模型响应应包含 `tool_calls` 字段 // 但我们的“最小版本”可能先让模型在 `content` 中以JSON文本形式返回指令 // 这里我们实现一个简化版:解析content中的JSON let toolCallCommand; try { toolCallCommand = JSON.parse(llmResponse.content); } catch (e) { // 如果解析失败,说明模型可能想直接回答 console.log(`模型返回了非JSON内容,视为最终回答: ${llmResponse.content}`); finalAnswer = llmResponse.content; break; } // 假设我们的协议是:{ "action": "call_tool", "tool_name": "...", "arguments": {...} } if (toolCallCommand.action === 'call_tool' && toolCallCommand.tool_name) { const toolName = toolCallCommand.tool_name; const toolArgs = toolCallCommand.arguments || {}; console.log(`模型决定调用工具: ${toolName}, 参数:`, toolArgs); // 3. 执行工具(“行动”) let toolResult; try { const tool = this.toolRegistry.getTool(toolName); toolResult = await tool.execute(toolArgs); } catch (toolError) { toolResult = `调用工具 "${toolName}" 时出错: ${toolError.message}`; } console.log(`工具执行结果: ${toolResult.substring(0, 100)}...`); // 4. 将结果作为“观察”添加到对话历史,供下一轮思考 // 格式很重要:告诉模型这是上次工具调用的结果 messages.push({ role: 'assistant', content: llmResponse.content, // 保留模型的原始指令 }); messages.push({ role: 'user', // 注意,这里用user角色来传递工具执行结果是一种常见技巧 content: `工具“${toolName}”的执行结果是:${toolResult}。请根据这个结果继续分析。`, }); } else if (toolCallCommand.action === 'final_answer') { // 模型决定给出最终答案 finalAnswer = toolCallCommand.answer; break; } else { // 无法理解的指令,作为错误观察返回 messages.push({ role: 'assistant', content: llmResponse.content, }); messages.push({ role: 'user', content: `我无法理解你的指令:${llmResponse.content}。请明确指定要调用的工具或给出最终答案。`, }); } } if (finalAnswer === null) { finalAnswer = `已达到最大迭代次数(${this.maxIterations}),未能得出最终结论。`; } return finalAnswer; } } export default AgentExecutor;为什么循环逻辑如此设计?
- 迭代限制(maxIterations):这是安全阀。没有它,如果模型陷入“幻觉”循环,不断调用无效工具,程序将永不停歇。5-10次是一个合理的起始值。
- 消息历史的构建:这是实现多轮对话和连贯思考的关键。我们必须把“模型指令 -> 工具结果”这个配对,完整地放回对话历史中。这里采用了一种简化模式:将工具结果伪装成
user的新消息。更复杂的实现会使用tool角色(如果API支持)。 - 错误处理与鲁棒性:对工具执行失败、模型返回非预期格式等情况都做了处理,并将错误信息反馈给模型。这模拟了人类遇到问题时的调整过程,让Agent有机会自我纠正。
- 简化协议:我们没有直接使用OpenAI的
tool_calls字段,而是让模型返回一个自定义的JSON。这样做虽然功能上不如原生支持强大,但极大地降低了对模型版本和API的依赖,让我们的最小版本更容易理解和调试,也更容易适配不同厂商的API。
3. 实战组装:构建你的第一个代码助手Agent
现在,让我们把大脑、工具库和执行引擎组装起来,创建一个能回答简单代码问题的迷你Cursor。
3.1 项目初始化与环境准备
首先,确保你的环境已安装Node.js(建议LTS版本,如18.x或20.x,避免使用v24.19.0这种未发布的版本)。创建一个新目录并初始化项目。
mkdir mini-cursor-agent && cd mini-cursor-agent npm init -y npm install node-fetch接着,创建我们之前设计好的几个核心文件:llmClient.js,toolRegistry.js,agentExecutor.js。
然后,我们需要创建几个具体的工具。除了之前的search_code,我们再添加两个实用的:
// myTools.js import { Tool } from './toolRegistry.js'; // 工具1:生成代码片段 const generateCodeTool = new Tool( 'generate_code', '根据功能描述和编程语言,生成相应的代码片段。', { type: 'object', properties: { description: { type: 'string', description: '需要实现的功能描述,例如“一个反转字符串的函数”', }, language: { type: 'string', description: '编程语言,如 javascript, python', default: 'javascript', }, }, required: ['description'], }, async ({ description, language }) => { // 这里同样是模拟。真实场景会调用一个代码生成模型API。 const mockCode = ` // ${description} (${language}) function reverseString(str) { return str.split('').reverse().join(''); } // 使用示例 console.log(reverseString('hello')); // 输出: 'olleh' `; return mockCode; } ); // 工具2:解释代码 const explainCodeTool = new Tool( 'explain_code', '解释一段给定代码的功能、逻辑或潜在问题。', { type: 'object', properties: { code_snippet: { type: 'string', description: '需要解释的代码片段', }, focus: { type: 'string', description: '关注点,如“功能”、“时间复杂度”、“潜在bug”', default: '功能', }, }, required: ['code_snippet'], }, async ({ code_snippet, focus }) => { const explanation = `这段代码看起来是一个${focus === '功能' ? '实现某种算法的函数' : '需要分析的结构'}。\n` + `(模拟解释)它接收输入,经过一系列处理,然后返回结果。\n` + `对于“${focus}”的详细分析,需要更具体的上下文。`; return explanation; } ); export { generateCodeTool, explainCodeTool };3.2 编写系统提示词(System Prompt)
系统提示词是Agent的“人格”和“行为准则”。一个好的提示词能极大提升工具调用的准确性和效率。
// systemPrompt.js const SYSTEM_PROMPT = `你是一个专业的代码助手Agent。你的目标是帮助用户解决编程问题。 你拥有以下工具: - search_code: 用于在代码库中搜索。 - generate_code: 用于生成代码片段。 - explain_code: 用于解释代码。 请遵循以下规则: 1. 首先,理解用户的请求。判断是否需要使用工具,以及使用哪个工具。 2. 如果需要使用工具,你必须以严格的JSON格式回复,且只包含这个JSON对象,不要有任何其他文字。 JSON格式必须为: { "action": "call_tool", "tool_name": "工具名称", "arguments": { /* 工具所需的参数对象 */ } } 确保参数的值符合工具描述中的要求。 3. 当你根据工具返回的结果,已经能得出明确答案时,请使用以下JSON格式给出最终答案: { "action": "final_answer", "answer": "你的最终答案文本" } 4. 如果用户的问题不需要工具就能直接回答,或者工具结果已经足够,请直接给出最终答案。 5. 保持回答简洁、专业。 现在,开始帮助用户吧。`; export default SYSTEM_PROMPT;提示词设计心得:
- 角色定位清晰:开宗明义,“你是一个专业的代码助手Agent”。
- 工具清单:明确列出可用工具及其核心用途,帮助模型建立认知。
- 输出格式强制:这是最关键的部分。用清晰、无歧义的语言规定模型必须返回JSON,并给出精确的模板。这大大减少了模型“胡说八道”的概率。
- 流程指导:告诉模型先判断,再调用,最后总结。这模拟了ReAct的思考链。
- 风格要求:“简洁、专业”这类指令有助于统一输出风格。
3.3 主程序:将所有部分连接起来
最后,我们创建一个index.js作为入口点。
// index.js import LLMClient from './llmClient.js'; import { ToolRegistry, searchCodeTool } from './toolRegistry.js'; import { generateCodeTool, explainCodeTool } from './myTools.js'; import AgentExecutor from './agentExecutor.js'; import SYSTEM_PROMPT from './systemPrompt.js'; import dotenv from 'dotenv'; // 加载环境变量,用于存储API Key dotenv.config(); async function main() { // 1. 初始化LLM客户端 (请将你的API Key放入.env文件) const apiKey = process.env.DEEPSEEK_API_KEY; if (!apiKey) { console.error('错误:请在项目根目录创建 .env 文件,并设置 DEEPSEEK_API_KEY=your_key_here'); process.exit(1); } const llmClient = new LLMClient(apiKey, 'https://api.deepseek.com'); // 以DeepSeek为例 // 2. 初始化工具注册表并注册工具 const toolRegistry = new ToolRegistry(); toolRegistry.register(searchCodeTool); toolRegistry.register(generateCodeTool); toolRegistry.register(explainCodeTool); console.log(`已注册工具: ${toolRegistry.getAllTools().map(t => t.name).join(', ')}`); // 3. 初始化执行引擎 const agent = new AgentExecutor(llmClient, toolRegistry, 5); // 4. 运行一个示例查询 const userQuery = "帮我用JavaScript写一个函数,计算斐波那契数列的第n项。"; console.log(`\n用户问题: ${userQuery}`); console.log('='.repeat(50)); try { const answer = await agent.run(userQuery, SYSTEM_PROMPT); console.log('\n' + '='.repeat(50)); console.log('Agent最终回答:'); console.log(answer); } catch (error) { console.error('运行Agent时出错:', error); } } main();在项目根目录创建.env文件:
DEEPSEEK_API_KEY=your_actual_api_key_here运行它:
node index.js你将看到类似以下的输出(由于工具是模拟的,输出是固定的):
已注册工具: search_code, generate_code, explain_code 用户问题: 帮我用JavaScript写一个函数,计算斐波那契数列的第n项。 ================================================== === 第 1 轮迭代 === 模型决定调用工具: generate_code, 参数: { description: '计算斐波那契数列的第n项', language: 'javascript' } 工具执行结果: // 计算斐波那契数列的第n项 (javascript) function fibonacci(n) { if (n <= 1) return n; let a = 0, b = 1; for (let i = 2; i <= n; i++) { [a, b] = [b, a + b]; } return b; } // 使用示例 console.log(fibonacci(10)); // 输出: 55 ... === 第 2 轮迭代 === 模型返回了非JSON内容,视为最终回答: 我已经使用 generate_code 工具为你生成了计算斐波那契数列第n项的JavaScript函数。该函数使用迭代方法,时间复杂度为O(n),空间复杂度为O(1)。你可以直接使用它。 ================================================== Agent最终回答: 我已经使用 generate_code 工具为你生成了计算斐波那契数列第n项的JavaScript函数。该函数使用迭代方法,时间复杂度为O(n),空间复杂度为O(1)。你可以直接使用它。恭喜!你已经成功运行了一个最小化的、具备工具调用能力的AI Agent。它接收自然语言指令,自主选择并调用了generate_code工具,然后将结果整合后返回给了你。
4. 从“玩具”到“工具”:关键优化与深度思考
我们实现了一个能跑通的核心循环,但这离一个健壮的、可用的“Cursor最小版本”还有距离。以下是几个必须考虑的优化方向和深度思考。
4.1 提升工具调用可靠性:从文本JSON到原生Function Calling
我们之前让模型返回文本JSON,然后手动解析。这是一种兼容性很强的方案,但不够优雅,也容易因模型输出格式轻微偏差而失败。更现代的做法是利用大模型原生的“函数调用”(Function Calling)或“工具调用”(Tool Calling)能力。
以支持Tool Calling的API(如OpenAI GPT-4, DeepSeek最新版本)为例,我们可以在请求中直接传入工具定义,模型会在响应中返回一个结构化的tool_calls数组。
我们需要升级LLMClient的chatCompletion方法和AgentExecutor的解析逻辑:
// 在LLMClient.chatCompletion中,将工具定义传入 const body = { model, messages, temperature, tools: tools, // 这里是关键,传入工具定义数组 tool_choice: "auto", // 让模型自行决定是否调用工具 }; // 在AgentExecutor中,解析响应 const llmResponse = await this.llm.chatCompletion(requestMessages, tools); // 传入tools // 解析方式改变 if (llmResponse.tool_calls && llmResponse.tool_calls.length > 0) { // 模型要求调用工具 const toolCall = llmResponse.tool_calls[0]; const toolName = toolCall.function.name; const toolArgs = JSON.parse(toolCall.function.arguments); // ... 后续执行工具的逻辑 } else { // 模型直接回复内容 finalAnswer = llmResponse.content; }这样做的好处:
- 更高的可靠性:模型原生支持,格式错误率极低。
- 支持并行调用:
tool_calls是一个数组,理论上模型可以一次性要求调用多个工具(虽然实践中较少)。 - 更清晰的协议:无需在提示词中硬编码复杂的JSON输出格式。
需要注意的坑:
- 成本:使用Tool Calling通常会让输入tokens略微增加(因为工具定义也作为输入)。
- 模型支持度:并非所有模型都支持此功能,需要查阅对应API文档。
- 错误处理:即使使用原生调用,模型仍可能返回参数不完整或格式错误的JSON,需要在执行前增加参数验证。
4.2 设计更强大的系统提示词与思维链
我们之前的系统提示词比较简单。为了提升Agent的推理能力,可以引入更复杂的提示工程技术。
1. 思维链(Chain-of-Thought, CoT)提示: 鼓励模型在调用工具前,先在内容中“思考”一下。虽然我们要求最终输出是JSON,但可以允许模型在content字段里先进行推理。
const SYSTEM_PROMPT_ADVANCED = `你是一个代码助手。请按以下步骤工作: 1. **分析问题**:仔细阅读用户请求,确定核心需求。 2. **规划步骤**:思考是否需要搜索现有代码、生成新代码或解释代码。如果需要多个步骤,规划顺序。 3. **选择工具**:根据步骤,选择最合适的工具。 4. **执行与总结**:调用工具,分析结果,并给出最终答案。 请在你的“思考”中体现步骤1和2。然后,如果需要工具,请严格按以下JSON格式输出: { "action": "call_tool", "tool_name": "...", "arguments": {...} } 如果可以直接回答或已有结论,请输出: { "action": "final_answer", "answer": "..." } 现在,开始处理请求。`;2. 提供少量示例(Few-Shot Prompting): 在系统提示词中直接给出一两个完整的输入-输出示例,能极大地引导模型遵循格式和逻辑。
const SYSTEM_PROMPT_WITH_EXAMPLES = ` 你是一个代码助手。请根据用户请求决定调用工具或直接回答。 示例1: 用户:查找项目中关于“用户验证”的代码。 助手思考:用户想搜索代码。我应该使用search_code工具。 助手输出:{"action": "call_tool", "tool_name": "search_code", "arguments": {"keyword": "用户验证"}} 示例2: 用户:上面的搜索结果里,那个`validateUser`函数是干什么的? 助手思考:用户想解释一段具体的代码。我应该使用explain_code工具,并引用搜索到的代码片段。 助手输出:{"action": "call_tool", "tool_name": "explain_code", "arguments": {"code_snippet": "function validateUser(email, password) { ... }", "focus": "功能"}} 请遵循以上格式和逻辑。现在处理新请求:`;4.3 实现持久化记忆与状态管理
我们当前的Agent是“无状态”的,每轮对话都是独立的。这对于简单任务够用,但对于复杂的、多轮交互的任务(比如调试一个bug,需要多次搜索、生成、修改),就需要记忆。
记忆可以分为两类:
- 短期记忆/对话历史:就是我们已经在
messages数组中维护的。关键在于如何高效地管理它,防止token数无限增长(导致API成本增加和模型上下文窗口溢出)。常见的策略是摘要(Summarization)或滑动窗口(只保留最近N轮对话)。 - 长期记忆/向量存储:将对话中的重要信息(如项目结构、已生成的代码片段、已解决的问题)转换成向量,存入像ChromaDB、Pinecone这样的向量数据库。当用户提出新问题时,先进行向量检索,把相关记忆作为上下文注入提示词。这才是LangChain RAG(检索增强生成)的核心思想在我们这个迷你项目中的体现。
实现一个简单的对话历史摘要功能:
// 在AgentExecutor中增加一个方法 async _summarizeHistory(messages) { // 当历史消息太长时,调用LLM生成一个摘要 const summaryPrompt = [ { role: 'system', content: '请将以下对话历史浓缩成一个简洁的摘要,保留关键决策、工具调用结果和未解决的问题。' }, { role: 'user', content: JSON.stringify(messages.slice(-6)) } // 摘要最近6条 ]; const summary = await this.llm.chatCompletion(summaryPrompt); // 返回摘要,用于替换旧的历史消息 return [{ role: 'system', content: `之前的对话摘要:${summary.content}` }]; }然后在run方法中,在每次迭代前检查messages的长度(或token数),如果超过阈值,就调用_summarizeHistory来压缩历史。
4.4 错误处理与调试:打造可观测的Agent
一个黑盒的Agent是可怕的。我们必须为其添加“眼睛”和“日志”,以便调试。
- 结构化日志:不要只用
console.log,使用像winston或pino这样的日志库,按级别(INFO, DEBUG, ERROR)记录关键事件:收到用户输入、模型请求/响应、工具调用开始/结束、最终输出。 - 跟踪与溯源:为每个用户会话或任务生成一个唯一ID,并将所有相关日志、中间结果(模型的原始响应、工具的参数和结果)关联起来。当出现“
deepseek returned tool calls without replayable thinking content”这种诡异错误时,你能快速定位到是哪次请求、什么上下文导致的。 - 超时与重试:为LLM API调用和工具执行设置超时。对于网络错误或速率限制,实现指数退避的重试机制。
- 验证与回退:在工具执行前,用JSON Schema验证器(如
ajv)检查模型提供的参数是否合规。如果不合规,可以将错误信息反馈给模型,要求它重新生成参数,而不是直接崩溃。
// 在Tool.execute方法中加入更详细的验证 const Ajv = require('ajv'); const ajv = new Ajv(); async execute(args) { // 1. 参数验证 const validate = ajv.compile(this.parameters); const valid = validate(args); if (!valid) { const errors = validate.errors.map(e => `${e.instancePath} ${e.message}`).join(', '); throw new Error(`工具参数验证失败: ${errors}`); } // 2. 执行并记录性能 const startTime = Date.now(); try { const result = await this._execute(args); const duration = Date.now() - startTime; logger.debug(`工具 ${this.name} 执行成功,耗时 ${duration}ms`); return result; } catch (error) { logger.error(`工具 ${this.name} 执行失败`, { args, error: error.message }); throw error; // 或返回一个特定的错误结果 } }通过以上四个方面的优化——使用原生工具调用、设计更好的提示词、引入记忆机制、加强可观测性——你的“最小版本”将从一个脆弱的原型,进化成一个真正有实用价值、可调试、可维护的AI Agent核心。这整个过程,正是理解LangChain这类框架内部奥秘的最佳路径。当你再遇到框架的复杂配置或诡异错误时,你就能从底层原理出发,更快地找到解决方案。