JS Agent实战指南:从零构建企业级AI智能体应用
1. 从零到一:理解AI Agent的核心与JS Agent的定位
最近几年,AI领域最让人兴奋的进展之一,无疑是“智能体”(Agent)概念的落地。它不再是科幻电影里遥不可及的幻想,而是我们能用代码亲手构建的、具备一定自主思考和行动能力的程序。简单来说,一个AI Agent就是一个能理解目标、规划步骤、使用工具(比如搜索网络、读写文件、执行命令)并最终完成任务的人工智能系统。你可以把它想象成一个数字世界的“实习生”,你给它一个任务,比如“帮我总结这篇论文的核心观点”,它就能自己去查资料、阅读、分析,最后给你一份摘要。
在这个领域,AutoGPT和BabyAGI是早期的明星项目,它们展示了让大语言模型(LLM)如GPT-4进行多步推理和行动的惊人潜力。然而,对于广大JavaScript和TypeScript开发者而言,直接上手这些项目可能会遇到一些门槛:环境配置复杂、代码结构不易理解、或者与自己熟悉的JS/TS技术栈集成困难。
这正是JS Agent出现的背景。它不是一个试图包罗万象的巨型框架,而是一个专为JS/TS生态设计的、可组合且可扩展的智能体构建工具包。它的核心设计哲学是:让构建一个Agent原型变得简单,同时提供坚实的“积木”和工具,帮助你快速迭代,将其打磨成可靠、健壮的生产级应用。虽然原作者已转向更通用的 ModelFusion 库进行持续开发,但JS Agent的架构思想和实现细节,对于任何想深入理解如何用JS/TS构建AI Agent的开发者来说,依然是一份极具价值的“遗产”和学习蓝本。
如果你是一名全栈或Node.js开发者,已经熟悉OpenAI API的基本调用,并且对打造能自动处理复杂流程的AI应用充满兴趣,那么深入剖析JS Agent,将是你从“调用单个API”迈向“构建自主智能系统”的关键一步。接下来,我将带你彻底拆解这个框架,不仅理解它怎么用,更要弄明白它为什么这样设计,以及在实际项目中我们可以如何借鉴和扩展。
2. 架构深度解析:JS Agent的设计哲学与核心模块
要真正用好一个框架,绝不能停留在“复制粘贴示例代码”的层面。我们必须深入其设计内核,理解作者在面对“构建可靠AI Agent”这一复杂问题时所做的取舍和抽象。JS Agent的架构清晰地反映了其应对复杂性的思路。
2.1 核心设计原则:为何选择这样的代码结构?
打开JS Agent的源码或示例,你会发现它强烈依赖于函数式编程(FP)风格,并提供了极好的TypeScript类型支持。这并非偶然,而是针对Agent开发中特有的挑战所做出的精心设计。
1. 不可变性与可组合性Agent的执行过程本质上是状态(State)的流转:从初始任务开始,经过一系列“思考-行动-观察”的循环,最终产生结果。这个过程中的每个步骤(Step)都可能产生副作用(如调用API、读写文件)。JS Agent采用函数式编程的核心思想——纯函数与不可变数据——来管理这种复杂性。
- 纯函数构建块:像
$.tool.executeExtractInformationFromWebpage、$.text.generateText.asFunction这样的函数,接收明确的输入,返回确定的输出,不依赖或改变外部隐藏状态。这使得每一个功能单元都像乐高积木,可以安全、灵活地组合。 - 不可变的运行状态:
AgentRun和Step对象虽然被标记为可能含有可变状态(这是对现实世界中IO操作和模型调用不确定性的妥协),但框架鼓励通过生成新的状态对象来推进流程,而非修改原有对象。这极大地简化了推理、调试和测试,因为你可以在任何时间点清晰地“冻结”并检查整个Agent的状态快照。
2. 渐进式细化与默认配置一个新手可能只想快速验证一个Agent想法,而一个经验丰富的工程师则需要精细控制每一步的提示词(Prompt)和模型参数。JS Agent通过“提供良好的默认值,同时允许深度定制”来满足这两种需求。 例如,$.prompt.extractChatPrompt()提供了一个用于信息提取的通用系统提示词。你可以直接使用它快速搭建流程。当你需要更精准的控制时,可以深入查看其实现,并用自定义的函数覆盖它,而无需重写整个流程。这种设计支持了从原型到产品的平滑演进。
3. 生产就绪的考量很多AI实验项目止步于Jupyter Notebook。JS Agent在诞生之初就考虑了生产环境的需求:
- 运行观测(Observer):
$.agent.observer.showRunInConsole可以将Agent的思考过程实时输出到控制台,便于调试。你可以轻松实现自己的Observer,将日志发送到ELK、或在前端UI中实时展示Agent的思考链路。 - 成本追踪:框架自动记录每一次LLM调用,并可以计算单次运行的总成本。这对于预算控制和优化提示词至关重要。
- 运行控制器(Controller):
$.agent.controller.maxSteps(20)是一个防止Agent陷入死循环或成本失控的安全阀。你可以实现更复杂的控制器,例如在检测到重复动作时自动停止。
2.2 核心模块拆解:Agent是如何运转的?
理解以下四个核心模块,你就掌握了JS Agent的命脉。
1. 工具(Tools & Actions):Agent的“手和脚”工具是Agent与外界交互的接口。JS Agent将工具抽象为Action。
// 定义一个工具:搜索维基百科 const searchWikipediaAction = $.tool.programmableGoogleSearchEngineAction({ id: "search-wikipedia", description: "Search wikipedia using a search term. Returns a list of pages.", execute: $.tool.executeProgrammableGoogleSearchEngineAction({ key, cx }), });关键点:
id和description:这两个字段至关重要。description会被拼接到给LLM的提示词中,用于让模型理解这个工具是干什么的、该怎么用。编写清晰、无歧义的description是提高工具被正确调用率的关键。execute函数:这是工具的具体实现。框架内置了读写文件、执行命令、网页抓取等常用工具。扩展性就体现在这里:你可以实现任何async (input) => output格式的函数,并将其包装成一个Action,比如调用内部API、操作数据库、发送邮件等。
2. 模型(Models):Agent的“大脑”JS Agent抽象了不同的LLM提供商和模型类型,提供了统一的调用接口。
import * as $ from "js-agent"; const openai = $.provider.openai; const chatModel = openai.chatModel({ apiKey: openAiApiKey, model: "gpt-3.5-turbo", // 或 "gpt-4" }); const textModel = openai.textModel({ apiKey: openAiApiKey, model: "text-davinci-003", }); const embeddingModel = openai.embeddingModel({ apiKey: openAiApiKey, model: "text-embedding-ada-002", });这种抽象的好处是,在你的核心Agent逻辑里,你操作的是统一的chatModel或textModel对象,而不是具体的API调用。如果未来需要切换模型提供商(例如到Anthropic或本地部署的模型),理论上只需更换Provider,业务逻辑改动最小。
3. 提示词(Prompts):与大脑沟通的“语言”提示词工程是Agent性能的决定性因素之一。JS Agent提供了强大的提示词组合能力。
const agentPrompt = $.prompt.concatChatPrompts( async ({ runState: { task } }) => [ { role: "system", content: `## ROLE You are an expert researcher... ## TASK ${task}`, }, ], $.prompt.availableActionsChatPrompt(), // 自动插入可用工具列表 $.prompt.recentStepsChatPrompt({ maxSteps: 6 }) // 自动插入最近几步历史,提供上下文 );$.prompt.concatChatPrompts是一个精髓设计。它允许你将角色设定、任务描述、工具列表、历史记录等模块化的提示词片段动态地组合成一个完整的对话上下文。这比手动拼接字符串要清晰、安全得多,也便于复用和A/B测试。
4. 运行循环(Loops):Agent的“思考节奏”这是驱动Agent一步步前进的引擎。JS Agent主要提供了两种循环策略:
- 生成下一步循环(Generate Next Step Loop):这是最常用、最直观的模式。在每一步,Agent根据当前状态和可用工具,决定下一步是使用某个工具,还是认为任务已完成并输出最终答案。示例中的Wikipedia Agent就采用此模式。
- BabyAGI风格的任务规划循环:更复杂的模式。Agent首先会创建一个任务列表,然后持续地执行任务、根据结果生成新任务、并优先处理最重要的任务。这适用于目标宏大、需要拆解为多个子问题的场景。
选择哪种循环,取决于你的任务特性。简单问答用“下一步循环”,复杂项目规划用“BabyAGI循环”。
3. 实战:从零构建一个可用的AI Agent
理论说得再多,不如亲手搭建一个。让我们抛开示例,设想一个实际场景:一个内部知识库问答Agent。假设公司有一个Markdown文档库,新员工经常需要查找信息。我们可以构建一个Agent,允许用户用自然语言提问,Agent自动搜索相关文档并提炼答案。
3.1 项目初始化与环境准备
首先,创建一个新的Node.js项目并安装依赖。
mkdir company-knowledge-agent && cd company-knowledge-agent npm init -y npm install js-agent openai npm install -D typescript ts-node @types/node # 初始化tsconfig.json npx tsc --init在tsconfig.json中,确保compilerOptions包含"esModuleInterop": true和"moduleResolution": "node",以更好地兼容CommonJS模块。
接下来,组织你的项目结构。一个清晰的结构有助于长期维护:
src/ ├── agents/ │ └── knowledgeAgent.ts # 主Agent逻辑 ├── tools/ │ ├── searchLocalDocs.ts # 自定义工具:搜索本地文档 │ └── readDocContent.ts # 自定义工具:读取文档内容 ├── prompts/ │ └── systemPrompts.ts # 存放系统提示词模板 ├── utils/ │ └── textSplitters.ts # 文本处理工具函数 └── index.ts # 应用入口点3.2 实现核心工具:搜索与读取本地文档
JS Agent内置了网络搜索工具,但我们的文档在本地。这就需要自定义工具。
工具一:本地文档搜索工具这个工具模拟一个简单的语义搜索。我们假设文档已经处理成向量并存储(例如使用ChromaDB或Pinecone),这里为了简化,我们实现一个基于关键词的文件名匹配搜索。
// src/tools/searchLocalDocs.ts import * as $ from "js-agent"; import fs from 'fs/promises'; import path from 'path'; // 假设文档库根目录 const DOCS_ROOT = path.join(__dirname, '../../company-docs'); export function createSearchLocalDocsAction() { return $.tool.action({ id: "search-local-docs", description: "Search for company documentation files by topic or keyword. Returns a list of relevant file paths and their brief titles.", inputExample: { query: "annual leave policy" }, execute: async ({ query }: { query: string }) => { // 1. 递归获取所有Markdown文件 const getAllMdFiles = async (dir: string): Promise<string[]> => { let files: string[] = []; const items = await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dir, item.name); if (item.isDirectory()) { files = files.concat(await getAllMdFiles(fullPath)); } else if (item.name.endsWith('.md')) { files.push(fullPath); } } return files; }; const allFiles = await getAllMdFiles(DOCS_ROOT); // 2. 简陋的匹配逻辑:检查文件名和路径是否包含查询词(实际应用应用嵌入向量) const relevantFiles = allFiles.filter(filePath => { const relativePath = path.relative(DOCS_ROOT, filePath); const fileName = path.basename(filePath, '.md').toLowerCase(); const queryTerms = query.toLowerCase().split(' '); return queryTerms.some(term => fileName.includes(term) || relativePath.toLowerCase().includes(term) ); }).slice(0, 5); // 返回最多5个最相关结果 // 3. 为每个文件生成一个简短的标题(例如,去掉扩展名,美化路径) const results = relevantFiles.map(filePath => ({ filePath, title: path.basename(filePath, '.md').replace(/-/g, ' '), relativePath: path.relative(DOCS_ROOT, filePath) })); return { success: true, output: `Found ${results.length} relevant document(s):\n` + results.map(r => `- "${r.title}" (path: ${r.relativePath})`).join('\n') }; }, }); }实操心得:工具设计的边界:在设计工具时,要仔细考虑输入输出的格式。输入应尽可能简单(如一个查询字符串),输出应结构化且包含足够信息供LLM理解,同时也要考虑工具可能失败的情况(网络超时、文件不存在),并在
execute函数中通过返回{ success: false, error: '...' }这样的结构来处理,而不是直接抛出异常,这能让Agent更稳健。
工具二:读取文档内容工具搜索工具返回了文件路径,现在需要读取具体内容。
// src/tools/readDocContent.ts import * as $ from "js-agent"; import fs from 'fs/promises'; import path from 'path'; export function createReadDocContentAction() { return $.tool.action({ id: "read-doc-content", description: "Read the full content of a specific company document given its file path. Use this to get detailed information after searching.", inputExample: { filePath: "hr/policies/leave-policy.md" }, execute: async ({ filePath }: { filePath: string }) => { const fullPath = path.join(DOCS_ROOT, filePath); try { const content = await fs.readFile(fullPath, 'utf-8'); // 可选:对超长文档进行智能截断或分块 const truncatedContent = content.length > 8000 ? content.substring(0, 8000) + '\n\n...[文档过长,已截断]' : content; return { success: true, output: `Content of "${filePath}":\n\n${truncatedContent}` }; } catch (error) { return { success: false, output: `Failed to read file at path "${filePath}": ${error.message}. Please check if the path is correct.` }; } }, }); }3.3 组装Agent:定义大脑、工具与运行逻辑
现在,我们将大脑(模型)、工具(手)和运行逻辑(工作流)组装起来。
// src/agents/knowledgeAgent.ts import * as $ from "js-agent"; import { createSearchLocalDocsAction } from '../tools/searchLocalDocs'; import { createReadDocContentAction } from '../tools/readDocContent'; const openai = $.provider.openai; export async function runKnowledgeAgent({ openAiApiKey, userQuestion, maxSteps = 10 }: { openAiApiKey: string; userQuestion: string; maxSteps?: number; }) { // 1. 实例化工具 const searchAction = createSearchLocalDocsAction(); const readAction = createReadDocContentAction(); // 2. 配置LLM模型 - 使用gpt-3.5-turbo以控制成本,对知识问答足够 const model = openai.chatModel({ apiKey: openAiApiKey, model: "gpt-3.5-turbo", temperature: 0.1, // 低温度,使回答更确定、更基于事实 maxTokens: 1000, }); // 3. 定义Agent提示词 const agentPrompt = $.prompt.concatChatPrompts( async ({ runState: { userQuestion } }) => [ { role: "system", content: `## ROLE You are a helpful internal assistant for our company. Your knowledge is strictly limited to the company's internal documentation. You must ONLY use information from the documents you read to answer questions. If you cannot find the answer in the documents, you must say so clearly. ## CONSTRAINTS - You MUST use the "search-local-docs" tool to find relevant documents first. - You MUST use the "read-doc-content" tool to read the content of documents before answering. - Your final answer must be concise, accurate, and cite the source document(s) you used. - If the search yields no results, or the documents do not contain the answer, state: "I couldn't find relevant information in the company documentation about that." ## TASK Answer the following question based solely on company docs: ${userQuestion}` }, ], $.prompt.availableActionsChatPrompt(), // 自动注入可用工具描述 $.prompt.recentStepsChatPrompt({ maxSteps: 4 }) // 注入最近4步历史,防止遗忘 ); // 4. 创建并运行Agent return $.runAgent<{ userQuestion: string }>({ properties: { userQuestion }, agent: $.step.generateNextStepLoop({ actions: [searchAction, readAction], actionFormat: $.action.format.flexibleJson(), // 让模型以JSON格式输出工具调用 prompt: agentPrompt, model: model, }), controller: $.agent.controller.maxSteps(maxSteps), // 安全阀,防止无限循环 observer: $.agent.observer.showRunInConsole({ name: "Company Knowledge Agent", // 可以自定义观察者,例如记录到文件或发送到监控系统 }), }); }3.4 创建应用入口并测试
最后,我们创建一个简单的CLI入口来测试这个Agent。
// src/index.ts import { runKnowledgeAgent } from './agents/knowledgeAgent'; import * as dotenv from 'dotenv'; dotenv.config(); async function main() { const openAiApiKey = process.env.OPENAI_API_KEY; if (!openAiApiKey) { console.error('错误:请设置 OPENAI_API_KEY 环境变量。'); process.exit(1); } // 从命令行参数获取问题,或使用默认问题 const userQuestion = process.argv.slice(2).join(' ') || "公司的年假政策是怎样的?"; console.log(`🤖 知识库助手启动,正在处理问题: "${userQuestion}"\n`); try { const result = await runKnowledgeAgent({ openAiApiKey, userQuestion, maxSteps: 8 }); console.log('\n' + '='.repeat(50)); console.log('✅ 任务完成!'); console.log('最终回答:'); console.log(result.finalAnswer || '(未生成最终答案)'); console.log('='.repeat(50)); // 可选:打印成本摘要 if (result.runCost) { console.log(`\n本次运行预估成本: $${result.runCost.toFixed(4)}`); } } catch (error) { console.error('❌ Agent运行失败:', error); } } main();在运行前,确保在项目根目录创建.env文件并设置你的OpenAI API密钥,同时创建一个模拟的company-docs文件夹,里面放一些Markdown文件。
OPENAI_API_KEY=sk-your-key-here然后运行:
npx ts-node src/index.ts "如何申请报销?"你将看到Agent在控制台中一步步地“思考”:调用搜索工具、列出文件、调用读取工具、分析内容,最后生成答案。这个过程清晰展示了AI Agent的自主推理链条。
4. 进阶技巧、避坑指南与生产化考量
构建一个能跑的Agent原型只是第一步。要让它在真实场景中可靠工作,还需要考虑很多细节。
4.1 提示词工程:让Agent更“听话”的秘诀
Agent的“智商”和“执行力”很大程度上取决于提示词。基于JS Agent的实践,我总结出几个关键技巧:
1. 角色(ROLE)与约束(CONSTRAINTS)必须清晰、强硬
- 角色要具体。不要用“你是一个助手”,要用“你是公司内部知识库专家,你的知识仅限于公司文档”。
- 约束要使用“必须(MUST)”、“禁止(MUST NOT)”等强动词。明确告诉它工具的调用顺序(“必须先搜索,再阅读”),以及回答的边界(“答案必须来自文档”)。这能显著减少模型的“幻觉”(即编造信息)。
2. 为工具提供高质量的描述和示例工具的description和inputExample是模型学习使用工具的“说明书”。
- 描述要说明工具的功能、输入格式和输出内容。例如:“根据主题关键词搜索本地文档库,返回相关文件的路径和标题列表。”
- 输入示例要展示一个典型的、结构化的JSON输入。这能引导模型输出正确格式的调用参数。
3. 管理上下文长度与历史LLM有上下文窗口限制。$.prompt.recentStepsChatPrompt({ maxSteps: 4 })只保留最近4步,这是一个权衡。保留太少,Agent可能忘记之前做了什么;保留太多,会挤占用于当前思考的令牌数,并增加成本。对于长对话或复杂任务,你需要设计更智能的“记忆”机制,比如将历史总结后再放入上下文。
4.2 错误处理与稳定性:构建健壮的Agent
AI应用天生具有不确定性。模型可能输出无法解析的JSON,工具可能调用失败,网络可能超时。
1. 工具执行的健壮性如前所述,工具函数内部应使用try...catch,并返回统一的{ success, output }结构。在Agent的循环逻辑中(虽然JS Agent内部已处理),理论上应该能处理工具失败的情况,让模型根据错误信息决定重试或调整策略。
2. 模型输出的解析与重试JS Agent使用$.action.format.flexibleJson()来解析模型输出的工具调用指令。但模型有时会输出非JSON文本或格式错误的JSON。框架层应该(并且JS Agent的设计也倾向于)对此进行重试。在实际开发中,你可以考虑:
- 实现解析后的验证:检查解析出的
action和input是否在允许的范围内。 - 设置重试与回退:对于解析失败,可以尝试让模型重新生成,或者使用更严格的提示词(例如,要求模型输出一个有效的JSON对象,且仅包含指定字段)。
3. 超时与中断对于长时间运行的任务(如处理大量文档),必须设置超时。JS Agent的maxSteps控制器是一种中断方式。你还可以实现一个基于运行时间的控制器$.agent.controller.maxTime(60000)(运行1分钟),或者组合多个控制器。
4.3 性能与成本优化
使用GPT-4运行一个多步Agent,成本可能迅速增加。以下是一些优化策略:
1. 模型选型策略
- 推理用轻量模型:对于决定下一步行动、总结文本等“推理”任务,
gpt-3.5-turbo在大多数情况下性价比极高,且速度更快。 - 关键生成用强大模型:对于需要高度创造性、严谨性或复杂推理的最终答案生成,可以切换到
gpt-4。JS Agent支持在同一个Agent运行中使用不同模型,你可以设计一个流程:用3.5做规划和工具调用,用4来做最终答案的精炼。
2. 文本处理的优化
- 智能分块与摘要:在
readDocContent工具中,我们对长文档进行了截断。更好的做法是使用框架内置的$.text.splitRecursivelyAtToken和$.text.extractRecursively。你可以先分块,然后让模型提取与问题相关的部分,最后只将相关部分放入上下文,这能大幅减少令牌消耗。 - 嵌入向量搜索:我们示例中的文件名搜索是简陋的。真实场景应使用嵌入模型(如
text-embedding-ada-002)为文档块生成向量,并建立向量数据库。搜索工具首先进行语义相似度搜索,返回最相关的几个文档块,再交给LLM阅读。这比全文阅读效率高几个数量级。
3. 缓存策略对于相同的查询和文档,结果往往是相同的。可以考虑对工具的结果(如搜索列表、文档内容摘要)进行缓存,甚至对某些确定的LLM调用结果进行缓存,以节省成本和提升响应速度。
4.4 监控、日志与调试
当Agent在后台自动运行时,完善的监控是保障服务质量的命脉。
1. 利用Observer系统JS Agent的observer参数是一个强大的钩子。除了内置的控制台输出,你一定要实现自己的Observer:
const myLoggerObserver = { onAgentRunStarted: (run) => console.log(`[${run.id}] Started`), onStepGenerated: (step) => console.log(`[${step.runId}] Step: ${step.type}`), onStepExecutionUpdated: (update) => { if (update.type === 'tool-call') { // 记录工具调用详情,包括输入输出,可用于审计和调试 logToElasticsearch(update); } }, onAgentRunFinished: (run) => { console.log(`[${run.id}] Finished. Cost: ${run.cost}`); sendAlertIfCostExceedsThreshold(run.cost); } };将这些日志与你的APM(如DataDog, New Relic)和日志聚合系统(如ELK)集成。
2. 记录完整的运行轨迹JS Agent的run对象包含了完整的步骤历史、LLM调用记录和成本。在运行结束时,务必将这些信息持久化到数据库(如MongoDB)。当用户反馈答案有误时,你可以通过runId回溯整个决策过程,精准定位是提示词问题、工具问题还是模型本身的问题。
3. 定义关键指标(KPIs)
- 成功率:任务成功完成的比率。
- 平均步骤数:衡量任务复杂度或提示词效率。
- 平均令牌消耗/成本:直接关联运营成本。
- 工具调用分布:了解哪些工具最常用,哪些可能存在问题。
- 用户满意度:通过简单的“是否解决您的问题?”反馈按钮收集。
5. 常见问题排查与实战心得
在实际开发和部署中,你一定会遇到各种奇怪的问题。这里记录了一些典型场景和解决思路。
5.1 Agent陷入循环或行为异常
症状:Agent反复调用同一个工具,或者执行与任务无关的操作。排查与解决:
- 检查提示词中的约束:是否足够强硬和清晰?模型是否理解了任务的边界?尝试在系统提示中加入“你必须避免重复执行完全相同的操作”。
- 审查工具描述:工具的
description是否含糊不清?确保描述准确说明了工具的用途和输出。一个模糊的描述会导致模型误用工具。 - 引入步骤历史:使用
$.prompt.recentStepsChatPrompt。如果模型“忘记”了自己刚做过什么,它就可能重复操作。适当增加maxSteps参数(例如从4增加到6)。 - 调整温度(Temperature):过高的温度(如0.8)会增加创造性,但也可能导致行为不稳定。对于需要确定性和遵循指令的Agent,将温度调低(如0.1-0.3)。
- 实现循环检测控制器:JS Agent允许自定义控制器。你可以实现一个控制器,检查最近N步的动作是否相同,如果相同则强制结束运行并返回错误。
5.2 工具调用失败或解析错误
症状:控制台日志显示模型输出了内容,但框架报错“无法解析动作”或工具执行抛出异常。排查与解决:
- 查看模型的原始输出:在Observer中打印出模型在每一步生成的完整响应。看看它是不是输出了非JSON内容,或者JSON格式正确但字段名与工具ID不匹配。
- 简化工具ID和输入:使用更简单、无空格的工具ID(如
search_docs而非search-local-docs)。确保inputExample中的字段名简单明了。 - 增强提示词引导:在系统提示中明确强调:“你必须以严格的JSON格式响应,只包含‘action’和‘input’两个字段。” 甚至可以给出一个更具体的模板。
- 在工具层加固:在工具的
execute函数中,对输入参数进行严格的类型验证和默认值处理,防止因意外输入导致程序崩溃。
5.3 响应速度慢或成本过高
症状:一个简单问题耗时数十秒,成本远超预期。排查与解决:
- 分析步骤日志:是每一步的LLM调用慢,还是某个工具执行(如网络请求)慢?LLM调用慢可能是模型负载高,可考虑重试或降级模型;工具执行慢则需要优化工具本身。
- 审查不必要的步骤:Agent是否进行了多余的搜索或阅读?优化提示词,鼓励其“一击即中”。例如:“在阅读文档前,尽量通过一次精准的搜索找到最相关的文件。”
- 实施上下文窗口管理:对于
readDocContent这类可能返回巨量文本的工具,一定要在工具内部进行截断、分块或摘要,绝不将超长文本直接塞给LLM。 - 设置预算和限制:务必使用
maxSteps和maxTokens参数。对于公开服务,可以考虑实现一个预算控制器,当单次会话成本超过某个阈值时自动终止。
5.4 扩展性挑战:如何添加新功能?
当你想为Agent增加新能力时,比如让它能发送邮件通知,JS Agent的模块化设计优势就体现出来了。
- 创建新工具:按照模式实现一个
sendEmailAction,在execute函数中调用你的邮件服务API。 - 更新Agent配置:将这个新Action加入到Agent的
actions数组中。 - 更新提示词:在系统提示的“可用工具”部分,自然会有新工具的
description。你可能还需要在角色描述或约束中补充一句:“当任务需要通知某人时,你可以使用发送邮件工具。”
整个过程几乎是在独立的模块中完成的,与现有核心逻辑耦合度极低。这正是可组合框架的魅力所在。
回顾整个从零构建AI Agent的旅程,从理解核心概念到拆解框架设计,再到亲手实现并优化一个生产可用的知识库助手,最关键的一课是:构建AI Agent是一个高度迭代的工程过程。没有一蹴而就的完美提示词,也没有放之四海皆准的工具链。你需要像训练一个真正的实习生一样,通过观察它的错误(日志),不断澄清你的指令(提示词),并为它配备更顺手的工具。
JS Agent虽然不再活跃开发,但它提供的这套基于函数式组合、强类型、关注生产就绪的架构范式,为TypeScript/JavaScript生态下的AI Agent开发树立了一个优秀的样板。它的思想完全可以在新的项目(如作者后续的ModelFusion)或你自己的定制化框架中延续。当你下次面对一个需要多步推理和外部交互的自动化需求时,不妨想想:是不是可以设计一个Agent,让AI来帮你完成这些复杂的决策链?
