代码优先AI智能体开发:用TypeScript构建可控、可测试的生产级应用
1. 项目概述:为什么我们需要一个“代码优先”的AI智能体开发套件?
如果你和我一样,在过去一两年里尝试过构建基于大语言模型的AI应用,大概率经历过这样的场景:一开始,你兴奋地打开某个低代码平台或聊天界面,通过拖拽和自然语言描述,快速“组装”出了一个能回答问题的智能体。它看起来很棒,能联网搜索、能调用API,你甚至把它分享给了同事。但当你试图把它集成到现有的业务系统里,或者想给它增加一个复杂的多步骤业务流程时,问题就来了。你会发现,智能体的行为逻辑像是一个黑盒,调试起来异常困难;你想用Git来管理它的版本迭代,却发现它的“代码”散落在平台的配置文件和数据库里;你想为它编写单元测试,以确保某个核心工具调用的稳定性,却发现无从下手。
这正是google/adk-js(Agent Development Kit for TypeScript)想要解决的核心痛点。它不是一个试图用自然语言取代一切的平台,而是一个回归开发者本源的代码优先(Code-First)工具包。它的核心理念是:构建复杂、可靠、可维护的AI智能体,最好的方式仍然是写代码。通过TypeScript这门在大型前端和后端项目中久经考验的语言,ADK让你能够像开发一个普通的Node.js服务一样,去定义智能体的思考逻辑、工具调用、状态管理和多智能体协作。你获得的是极致的灵活性和控制力,以及软件开发领域所有成熟的工程实践——版本控制、单元测试、CI/CD、代码审查——都可以无缝应用到你的AI智能体项目上。
简单来说,ADK-JS是为那些不满足于“玩具级”智能体,希望构建真正能在生产环境中运行、能与现有Google Cloud服务深度集成、并且其行为完全由代码定义和保障的开发者准备的。它把智能体从“配置”变成了“程序”。
1.1 核心设计哲学:控制权回归开发者
ADK-JS的设计哲学非常明确:将智能体行为的控制权完全交还给开发者。这与许多可视化编排平台形成了鲜明对比。在那些平台上,智能体的“推理过程”和“工具调用”往往被封装在平台内部,你只能通过有限的配置项和提示词去影响它,一旦出现非预期行为,排查成本很高。
ADK-JS则采用了另一种思路。它提供了一套清晰的、基于类的API,让你用TypeScript代码来显式地声明:
- 智能体是谁:它的名字、角色描述、使用的底层模型(如Gemini系列)。
- 智能体能做什么:它可以使用哪些工具(Tools)。这些工具可以是预置的(如谷歌搜索),也可以是你自己用TypeScript函数编写的任何业务逻辑。
- 智能体如何思考:通过结构化的指令(Instruction)来引导,但更重要的是,你可以通过代码介入它的决策循环。
- 智能体如何协作:如何设计多个智能体之间的交互流程,是顺序执行、并行处理还是基于事件驱动。
这种代码化的方式带来了几个立竿见影的好处:
- 可调试性:你可以在任何一步设置断点,查看智能体接收到的消息、内部的状态、准备调用的工具及其参数。整个推理链路对你是透明的。
- 可测试性:你可以为你的智能体编写单元测试和集成测试。例如,模拟一个用户问题,断言智能体一定会调用某个特定的工具并返回预期的结果。
- 可版本化:你的整个智能体应用就是一个标准的TypeScript项目,可以用Git进行版本管理。每一次逻辑变更都对应一次代码提交,回滚和代码对比变得轻而易举。
- 可集成性:智能体本身就是一段代码,你可以轻松地把它封装成一个函数、一个类,然后集成到现有的Express.js服务器、Next.js API路由或任何云函数中。
2. 核心概念深度解析:从零理解ADK-JS的架构
在开始动手写代码之前,我们需要先厘清ADK-JS中的几个核心抽象。理解这些概念,是灵活运用这个工具包的关键。
2.1 智能体(Agent):不仅仅是聊天机器人
在ADK-JS中,Agent(通常通过LlmAgent类来创建)是一个具有特定目标、能力和思考逻辑的实体。千万不要把它简单理解为一个聊天对话接口。它更像是一个可编程的、具备认知能力的Worker。
一个智能体由以下几个关键部分构成:
- 模型(Model):智能体背后的“大脑”。ADK-JS默认深度集成Google的Gemini模型(如
gemini-2.0-flash、gemini-2.5-pro),但你也可以通过配置连接到其他兼容的模型端点。模型的选择直接决定了智能体的基础认知能力和成本。 - 指令(Instruction):这是智能体的“角色设定”和“行为准则”。它比普通的系统提示词(System Prompt)更结构化,用于初始化智能体的上下文,告诉它“你是谁”、“你的目标是什么”、“你该如何行事”。好的指令是稳定智能体行为输出的基石。
- 工具(Tools):这是智能体能力的延伸。智能体本身不会写代码、查数据库、发邮件,但它可以通过“工具”来做到这些。工具是智能体与外部世界交互的桥梁。
2.2 工具(Tools):赋予智能体“手脚”
工具是ADK-JS中非常强大和灵活的概念。一个工具本质上是一个可以被智能体识别和调用的函数。ADK-JS支持多种形式的工具:
函数工具(Function Tool):这是最基础、最常用的形式。你可以将任何一个TypeScript异步函数(
async function)包装成一个工具。例如,一个查询数据库的函数、一个调用第三方天气API的函数、一个计算税费的函数。import { makeTool } from '@google/adk'; import { queryDatabase } from './my-database-client'; // 1. 定义你的业务函数 async function getUserOrderHistory(userId: string): Promise<Order[]> { // 这里是实际的数据库查询逻辑 return await queryDatabase(`SELECT * FROM orders WHERE user_id = ${userId}`); } // 2. 使用 makeTool 将其包装成ADK能识别的工具 const ORDER_HISTORY_TOOL = makeTool({ name: 'get_user_order_history', description: '根据用户ID查询该用户的历史订单列表。', parameters: { type: 'object', properties: { userId: { type: 'string', description: '用户的唯一标识符。' } }, required: ['userId'] }, // 将工具的执行指向你定义的函数 execute: getUserOrderHistory });关键点:
makeTool不仅包装了函数,更重要的是它利用JSON Schema定义了工具的输入参数。这个Schema会在大语言模型选择工具时被使用,模型需要生成符合这个Schema的参数来调用工具。这保证了调用的类型安全和结构正确。预置工具(Prebuilt Tools):ADK-JS开箱即用提供了一些常用工具,最典型的就是
GOOGLE_SEARCH。这让你无需自己处理搜索API的认证和结果解析,直接赋予智能体联网搜索能力。OpenAPI工具:如果你已经有一个遵循OpenAPI/Swagger规范的外部REST API,ADK-JS可以自动将其转化为智能体可用的工具集。这对于快速集成企业内部已有服务非常有用。
工具的使用哲学:你应该遵循“单一职责”原则来设计工具。一个工具最好只做一件事。例如,getUserProfile和updateUserProfile应该是两个独立的工具,而不是一个庞大的handleUser工具。这样能让智能体更精确地理解和使用它们,也便于你单独测试和维护。
2.3 多智能体系统(Multi-Agent Systems):从单兵到军团
单个智能体的能力是有限的。复杂的任务往往需要分工协作。ADK-JS的“代码优先”特性在构建多智能体系统时优势尽显。
你可以创建多个各司其职的智能体:
- 规划者(Planner):负责分解复杂任务,制定步骤。
- 执行者(Executor):负责调用具体工具完成任务。
- 审核者(Reviewer):负责检查执行结果的质量。
- 专家(Specialist):在特定领域(如数据分析、文案撰写)有专长。
然后,你通过TypeScript代码来编排它们之间的工作流。这种编排不是通过模糊的自然语言描述,而是通过清晰的程序逻辑,例如:
- 顺序链(Sequential Chain):智能体A完成任务后,将结果传递给智能体B。
- 路由(Router):根据输入内容的不同,决定由哪个智能体来处理。
- 并行与聚合(Parallel & Aggregate):多个智能体同时处理任务的子部分,最后汇总结果。
由于每个智能体都是代码定义的对象,这种编排就像是在组合函数或服务类,逻辑清晰,易于调试。ADK-JS未来与A2A(Agent-to-Agent)协议的集成,更是为了将这种协作能力扩展到跨网络、跨服务的分布式智能体之间。
3. 从零开始:构建你的第一个ADK-JS智能体
理论说得再多,不如动手一试。让我们从一个最简单的例子开始,构建一个能够查询天气的智能体。这个过程会清晰地展示ADK-JS“代码优先”的工作流。
3.1 环境准备与项目初始化
首先,确保你有一个Node.js环境(建议版本18+)。然后创建一个新的目录并初始化项目:
mkdir my-first-adk-agent && cd my-first-adk-agent npm init -y接下来,安装ADK-JS的核心包。由于我们需要调用一个真实的天气API,还需要安装axios(或其他你喜欢的HTTP客户端)。
npm install @google/adk axios npm install -D typescript @types/node tsx初始化TypeScript配置:
npx tsc --init你可以在生成的tsconfig.json中确保target是ES2020或更高,module是commonjs或nodenext。
3.2 定义自定义天气查询工具
智能体需要工具才能获取天气信息。我们将创建一个调用公开天气API的工具。
- 创建工具文件
src/tools/weatherTool.ts:
实操心得:在定义工具参数Schema时,import { makeTool } from '@google/adk'; import axios from 'axios'; // 这是一个真实的免费天气API示例(请在实际使用时查看其条款) const WEATHER_API_URL = 'https://api.open-meteo.com/v1/forecast'; async function getCurrentWeather(latitude: number, longitude: number): Promise<string> { try { const response = await axios.get(WEATHER_API_URL, { params: { latitude, longitude, current_weather: true, timezone: 'auto' } }); const data = response.data; const current = data.current_weather; // 将API响应格式化成对智能体友好的自然语言描述 return `当前位置当前天气:温度 ${current.temperature}°C,风速 ${current.windspeed} km/h,风向 ${current.winddirection}°,天气代码 ${current.weathercode}。`; } catch (error) { console.error('获取天气失败:', error); return '抱歉,暂时无法获取该位置的天气信息。'; } } // 使用 makeTool 创建工具 export const WEATHER_TOOL = makeTool({ name: 'get_current_weather', description: '根据给定的经纬度坐标,获取该位置的当前天气情况。', parameters: { type: 'object', properties: { latitude: { type: 'number', description: '地点的纬度,例如:52.52' }, longitude: { type: 'number', description: '地点的经度,例如:13.41' } }, required: ['latitude', 'longitude'] }, execute: getCurrentWeather });description字段至关重要。大语言模型依赖这个描述来理解每个参数的意义。描述应尽可能清晰,例如“纬度”比“lat”更好。此外,工具函数的返回值最好是结构化的字符串,方便智能体理解和整合到它的回答中。
3.3 创建并运行主智能体
现在,创建主文件src/index.ts,在这里实例化智能体并运行一个对话。
import { LlmAgent } from '@google/adk'; import { WEATHER_TOOL } from './tools/weatherTool'; // 1. 初始化智能体 const weatherAgent = new LlmAgent({ name: 'weather_expert', description: '一个专业的天气查询助手,可以根据用户提供的地点信息查询实时天气。', // 指定使用的模型,你需要配置相应的API密钥 model: 'gemini-2.0-flash', // 定义智能体的行为指令 instruction: `你是一个天气助手。你的核心能力是使用 get_current_weather 工具来查询精确的天气。 当用户询问某个地方的天气时,你需要引导用户提供具体的地理位置(最好是城市名,你能理解常见城市)。 如果用户只提供了城市名,你需要利用你的知识或上下文将其转换为大致的经纬度坐标(例如,北京约为 39.9, 116.4)。 一旦你获得了或推算出经纬度,就调用工具获取天气,并将结果用友好、易懂的方式总结给用户。 如果用户的问题与天气无关,请礼貌地告知你的能力范围。`, // 赋予智能体工具 tools: [WEATHER_TOOL], // 可选:配置模型参数,如创造力温度 modelConfig: { temperature: 0.2, // 较低的温度使输出更确定、更专注于工具调用 } }); // 2. 运行一个简单的对话 async function main() { console.log('天气助手已启动。输入“退出”或“quit”结束对话。\n'); // 在实际应用中,这里可能是从HTTP请求或聊天界面获取的用户输入 const userQuery = "今天北京的天气怎么样?"; console.log(`用户: ${userQuery}`); try { // 调用智能体的 `run` 方法处理用户输入 const response = await weatherAgent.run(userQuery); console.log(`助手: ${response.text}`); // 如果你想查看更详细的执行过程(例如工具调用记录),可以访问 `response` 的其他属性 // console.log('完整响应:', JSON.stringify(response, null, 2)); } catch (error) { console.error('处理请求时出错:', error); } } // 执行主函数 if (require.main === module) { main(); }3.4 配置与运行
要运行这个智能体,你需要一个Gemini API密钥。你可以通过Google AI Studio获取。
设置API密钥(通常通过环境变量):
export GOOGLE_API_KEY='你的API密钥'或者在代码中直接配置(不推荐用于生产环境):
import { configure } from '@google/adk'; configure({ apiKey: process.env.GOOGLE_API_KEY });运行智能体: 由于我们使用了
tsx来直接运行TypeScript,可以使用以下命令:npx tsx src/index.ts如果一切正常,你会看到控制台输出智能体对“北京天气”的回应。智能体会根据指令,将“北京”映射为经纬度,调用我们定义的
WEATHER_TOOL,获取真实天气数据,并生成最终回答。
注意:在开发过程中,你可能会频繁调整指令(Instruction)和工具描述。每次调整后,观察智能体行为的变化,是优化其表现的关键。ADK-JS提供的开发UI(通过
@google/adk/web包提供)是一个强大的可视化工具,可以让你实时看到智能体的内部思考过程、工具调用请求和响应,极大提升了调试效率。
4. 进阶实战:构建一个协作型多智能体客服系统
单一功能的智能体展示了基础能力。现在,让我们设计一个更贴近真实场景的例子:一个简易的电商客服系统。这个系统由两个智能体协作完成:
- 分类器(Classifier Agent):负责理解用户意图,将问题分类(如“查询订单”、“退货申请”、“产品咨询”)。
- 专家处理器(Specialist Agent):根据分类结果,由专门的智能体处理具体问题。例如,订单查询专家拥有查询数据库的工具。
这个例子将展示如何用代码编排智能体间的协作。
4.1 定义工具与模拟数据
首先,创建一些模拟工具和数据源。在src/tools/下创建新文件:
src/tools/orderTools.ts:
import { makeTool } from '@google/adk'; // 模拟一个订单数据库 const mockOrders = [ { orderId: 'ORD-1001', userId: 'user123', product: '无线耳机', status: '已发货', estimatedDelivery: '2023-10-30' }, { orderId: 'ORD-1002', userId: 'user123', product: 'Type-C数据线', status: '待付款' }, { orderId: 'ORD-1003', userId: 'user456', product: '编程书籍', status: '已完成' }, ]; export const GET_ORDER_TOOL = makeTool({ name: 'get_order_status', description: '根据订单号查询订单的详细状态和物流信息。', parameters: { type: 'object', properties: { orderId: { type: 'string', description: '订单编号,例如 ORD-1001' } }, required: ['orderId'] }, execute: async ({ orderId }) => { const order = mockOrders.find(o => o.orderId === orderId); if (!order) { return `未找到订单号 ${orderId} 的记录。`; } return `订单 ${orderId} 状态:${order.status}。商品:${order.product}。${order.estimatedDelivery ? `预计送达时间:${order.estimatedDelivery}` : ''}`; } }); export const GET_USER_ORDERS_TOOL = makeTool({ name: 'get_user_orders', description: '根据用户ID查询该用户的所有历史订单概要。', parameters: { type: 'object', properties: { userId: { type: 'string', description: '用户ID' } }, required: ['userId'] }, execute: async ({ userId }) => { const userOrders = mockOrders.filter(o => o.userId === userId); if (userOrders.length === 0) { return `用户 ${userId} 暂无订单记录。`; } const summary = userOrders.map(o => `- ${o.orderId}: ${o.product} (${o.status})`).join('\n'); return `用户 ${userId} 的订单列表:\n${summary}`; } });src/tools/productTools.ts:
import { makeTool } from '@google/adk'; const mockProducts = [ { id: 'P-001', name: '无线耳机', price: 199, stock: 45 }, { id: 'P-002', name: '机械键盘', price: 399, stock: 12 }, ]; export const GET_PRODUCT_INFO_TOOL = makeTool({ name: 'get_product_info', description: '根据产品名称或ID查询产品详情,包括价格和库存。', parameters: { type: 'object', properties: { productIdentifier: { type: 'string', description: '产品名称或ID,例如“无线耳机”或“P-001”' } }, required: ['productIdentifier'] }, execute: async ({ productIdentifier }) => { const product = mockProducts.find(p => p.id === productIdentifier || p.name.includes(productIdentifier)); if (!product) { return `未找到产品“${productIdentifier}”的信息。`; } return `产品:${product.name},价格:${product.price}元,库存:${product.stock > 0 ? `有货(${product.stock}件)` : '缺货'}。`; } });4.2 创建专业化智能体
接下来,创建两个专家智能体,每个都装备了其专属的工具集。
src/agents/orderSpecialist.ts:
import { LlmAgent } from '@google/adk'; import { GET_ORDER_TOOL, GET_USER_ORDERS_TOOL } from '../tools/orderTools'; export const orderSpecialist = new LlmAgent({ name: 'order_specialist', description: '专门处理所有与订单相关查询的专家,如查询状态、历史订单等。', model: 'gemini-2.0-flash', instruction: `你是订单查询专家。你只能处理与订单相关的问题。 你可以通过订单号查询单个订单的详细状态,也可以通过用户ID查询其所有订单。 当用户提供信息不足时(例如只问“我的订单怎么样了”),你需要礼貌地询问具体的订单号或用户ID。 你的回答应清晰、准确、友好。`, tools: [GET_ORDER_TOOL, GET_USER_ORDERS_TOOL], modelConfig: { temperature: 0.1 } // 订单查询需要非常准确 });src/agents/productSpecialist.ts:
import { LlmAgent } from '@google/adk'; import { GET_PRODUCT_INFO_TOOL } from '../tools/productTools'; export const productSpecialist = new LlmAgent({ name: 'product_specialist', description: '专门处理产品信息查询的专家,如价格、库存、规格等。', model: 'gemini-2.0-flash', instruction: `你是产品信息专家。你只能回答关于产品详情的问题,例如价格、库存、是否有货等。 如果用户询问的产品不存在或信息不足,请如实告知。 不要回答与产品无关的问题。`, tools: [GET_PRODUCT_INFO_TOOL], modelConfig: { temperature: 0.1 } });4.3 构建路由分类器智能体
分类器智能体本身不处理具体业务,它的核心职责是分析用户输入,并决定将任务路由给哪个专家。在这个简单示例中,我们让分类器直接输出一个决策。
src/agents/classifier.ts:
import { LlmAgent } from '@google/adk'; export const classifier = new LlmAgent({ name: 'intent_classifier', description: '分析用户意图并将其分类到特定处理类别。', model: 'gemini-2.0-flash', instruction: `你的任务是对用户的客服请求进行精准分类。 请只输出一个且仅一个分类标签,不要输出任何其他解释。 可用的分类标签有: - "ORDER_INQUIRY": 当用户的问题涉及订单状态、物流、历史订单查询时。 - "PRODUCT_INQUIRY": 当用户的问题涉及产品价格、库存、规格、是否有货时。 - "OTHER": 当用户的问题不属于以上任何类别时(如投诉、建议、闲聊)。 示例: 用户:“我的订单ORD-1001到哪了?” -> ORDER_INQUIRY 用户:“无线耳机多少钱?” -> PRODUCT_INQUIRY 用户:“你们客服电话是多少?” -> OTHER `, // 分类器不需要具体业务工具 tools: [], modelConfig: { temperature: 0.0 } // 温度设为0,让分类结果尽可能确定 });4.4 实现协作编排逻辑
最后,在src/orchestrator.ts中编写协调所有智能体的“大脑”。这是体现“代码优先”威力的地方——整个工作流由清晰的程序逻辑控制。
import { classifier } from './agents/classifier'; import { orderSpecialist } from './agents/orderSpecialist'; import { productSpecialist } from './agents/productSpecialist'; // 定义分类结果类型 type Intent = 'ORDER_INQUIRY' | 'PRODUCT_INQUIRY' | 'OTHER'; export class CustomerServiceOrchestrator { /** * 处理用户查询的主流程 * 1. 分类 -> 2. 路由 -> 3. 专家处理 -> 4. 返回结果 */ async handleUserQuery(userInput: string): Promise<string> { console.log(`[Orchestrator] 处理查询: "${userInput}"`); // 步骤1:意图分类 const intent = await this.classifyIntent(userInput); console.log(`[Orchestrator] 分类结果: ${intent}`); // 步骤2 & 3:根据分类路由到对应专家 let finalResponse: string; switch (intent) { case 'ORDER_INQUIRY': finalResponse = await orderSpecialist.run(userInput); break; case 'PRODUCT_INQUIRY': finalResponse = await productSpecialist.run(userInput); break; case 'OTHER': default: finalResponse = `您好!我主要擅长处理订单查询和产品咨询。您的问题“${userInput}”可能涉及其他客服领域,我已为您记录,稍后会有专人联系您。您也可以直接拨打客服热线 400-XXX-XXXX。`; break; } // 步骤4:返回最终响应 return typeof finalResponse === 'string' ? finalResponse : finalResponse.text; } /** * 调用分类器智能体分析意图 * 这里对分类器的输出做了简单清洗,确保返回预定义的标签。 */ private async classifyIntent(userInput: string): Promise<Intent> { const classification = await classifier.run(userInput); const rawText = classification.text.trim(); // 清洗输出,提取标签 if (rawText.includes('ORDER_INQUIRY')) return 'ORDER_INQUIRY'; if (rawText.includes('PRODUCT_INQUIRY')) return 'PRODUCT_INQUIRY'; // 默认归类为 OTHER return 'OTHER'; } }4.5 运行与测试协作系统
创建一个src/main.ts来测试整个系统:
import { CustomerServiceOrchestrator } from './orchestrator'; async function main() { const orchestrator = new CustomerServiceOrchestrator(); const testQueries = [ "订单ORD-1001现在是什么状态?", "user123的所有订单能帮我查一下吗?", "机械键盘还有货吗?多少钱?", "我想投诉上周的配送服务。", "无线耳机", ]; for (const query of testQueries) { console.log(`\n=== 用户问题: "${query}" ===`); const response = await orchestrator.handleUserQuery(query); console.log(`客服回复: ${response}`); console.log('---'); } } main().catch(console.error);运行npx tsx src/main.ts,你会看到系统如何自动将不同问题路由给不同的专家智能体处理。例如,“订单ORD-1001”会触发orderSpecialist调用GET_ORDER_TOOL;而“机械键盘”会触发productSpecialist调用GET_PRODUCT_INFO_TOOL。
这个例子虽然简单,但清晰地展示了ADK-JS的核心优势:
- 模块化:每个智能体和工具都是独立的模块,易于开发和测试。
- 透明可控:整个决策流程(分类 -> 路由 -> 处理)都在你的代码控制之下,可以轻松添加日志、监控和错误处理。
- 易于扩展:如果需要增加一个“退货处理专家”,你只需要创建新的工具和智能体,然后在
Orchestrator的switch语句中添加一个新的case即可。
5. 工程化实践:测试、调试与部署
构建出可用的智能体只是第一步。要将其用于生产,我们必须考虑工程化实践。ADK-JS的代码优先特性,让这些实践变得非常自然。
5.1 为你的智能体编写单元测试
由于智能体和工具都是纯函数或类,你可以像测试普通代码一样测试它们。使用Jest、Vitest等测试框架即可。
示例:测试天气工具 (src/tools/weatherTool.test.ts):
import { WEATHER_TOOL } from './weatherTool'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; // 创建axios模拟实例 const mockAxios = new MockAdapter(axios); describe('WeatherTool', () => { afterEach(() => { mockAxios.reset(); }); it('应该使用正确的参数调用天气API并返回格式化的字符串', async () => { // 1. 模拟API成功响应 const mockResponse = { current_weather: { temperature: 22.5, windspeed: 10.2, winddirection: 180, weathercode: 0 } }; mockAxios.onGet('https://api.open-meteo.com/v1/forecast').reply(200, mockResponse); // 2. 执行工具函数 const result = await WEATHER_TOOL.execute({ latitude: 52.52, longitude: 13.41 }); // 3. 断言 expect(mockAxios.history.get[0].params).toEqual({ latitude: 52.52, longitude: 13.41, current_weather: true, timezone: 'auto' }); expect(result).toContain('22.5'); expect(result).toContain('10.2'); }); it('应该在API调用失败时返回友好的错误信息', async () => { // 模拟API失败 mockAxios.onGet('https://api.open-meteo.com/v1/forecast').reply(500); const result = await WEATHER_TOOL.execute({ latitude: 0, longitude: 0 }); expect(result).toBe('抱歉,暂时无法获取该位置的天气信息。'); }); });测试心得:对于工具,重点测试其边界条件(如无效输入)、错误处理(如网络异常)和输出格式。对于智能体,可以模拟工具调用,测试其在特定指令下,对于给定输入是否会产生预期的工具调用序列和最终回复。
5.2 利用开发UI进行可视化调试
ADK-JS提供了一个本地开发UI,这是调试智能体行为的利器。它允许你以Web界面的形式与智能体交互,并实时查看其完整的“思考链”(Chain-of-Thought),包括:
- 模型接收到的完整提示词。
- 模型决定调用哪个工具(及其理由)。
- 工具被调用时的具体参数。
- 工具返回的原始结果。
- 模型根据工具结果生成的最终回复。
要启动开发UI,通常需要安装@google/adk/web包并运行一个本地服务器。这让你能像使用ChatGPT界面一样与你的智能体对话,但背后是所有可审查的详细日志,极大降低了调试复杂度。
5.3 部署策略:从本地到云
你的ADK-JS智能体项目本质上是一个Node.js应用,因此所有标准的Node.js部署方式都适用。
打包:使用
npm run build将TypeScript编译为JavaScript。容器化(推荐):创建Dockerfile,将构建产物和依赖打包成镜像。这确保了环境一致性。
FROM node:18-slim WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY dist ./dist CMD ["node", "dist/index.js"]部署目标:
- Google Cloud Run:这是与Google生态集成最紧密的无服务器方案。你可以将容器镜像部署到Cloud Run,它会自动处理扩缩容。ADK智能体可以轻松调用其他Google Cloud服务(如Cloud SQL, Vertex AI等)。
- Google Cloud Functions:对于更轻量级的、事件驱动的场景,可以将智能体逻辑封装成云函数。
- 任何Node.js托管环境:你也可以将应用部署在Google Compute Engine、GKE,甚至其他云服务商或你自己的服务器上。
环境配置:务必通过环境变量来管理敏感信息,如
GOOGLE_API_KEY、数据库连接字符串等。在Cloud Run或Cloud Functions中,这可以很方便地完成。
5.4 监控与日志
在生产环境中,你需要监控智能体的性能、成本和错误。
- 日志:在工具函数和智能体编排逻辑的关键节点添加结构化日志(例如使用Winston或Pino)。记录用户输入、工具调用参数、模型使用token数、最终输出以及任何错误。
- 指标:考虑收集一些业务指标,如“用户问题分类分布”、“各工具调用成功率”、“平均响应延迟”。这些数据可以帮助你优化智能体指令和工具设计。
- 成本控制:监控模型API的调用量和费用。可以为智能体设置预算告警,或者在代码层面实现限流和降级策略。
6. 常见问题、排查技巧与最佳实践实录
在实际使用ADK-JS的过程中,你肯定会遇到各种问题。以下是我从项目实践中总结的一些常见坑点和解决思路。
6.1 智能体不调用工具或调用错误工具
这是最常见的问题之一。
可能原因1:工具描述不清晰。
- 排查:检查
makeTool中的description和每个参数的description。这些描述是模型理解工具用途的唯一依据。描述要具体、无歧义,说明工具做什么以及何时使用。 - 技巧:在描述中可以使用“Use this tool when the user asks about...”这样的句式来引导模型。对比一下:
- 不佳:
description: 'Gets weather.' - 优秀:
description: 'Use this tool to get the current temperature, wind speed, and conditions for a specific geographic location. Always call this when the user asks "what's the weather like in [city]?" or similar.'
- 不佳:
- 排查:检查
可能原因2:指令(Instruction)与工具能力不匹配。
- 排查:智能体的
instruction必须明确告知它拥有这些工具,并鼓励或指导它在合适场景下使用。如果指令过于笼统或完全没提工具,模型可能不会主动调用。 - 技巧:在指令中明确列出可用的工具名,并给出使用示例。例如:“你可以使用
get_current_weather工具来查询任何地点的天气。当用户询问天气时,你应该主动调用这个工具。”
- 排查:智能体的
可能原因3:模型温度(Temperature)设置过高。
- 排查:
modelConfig.temperature控制输出的随机性。值越高(如0.8),回答越有创意但也越不稳定;值越低(如0.1),回答越确定、可预测。对于需要精确工具调用的任务,建议设置为较低的值(0.1-0.3)。 - 技巧:在开发调试阶段,可以暂时将温度设为0,让模型输出最可能的确定性结果,这有助于判断是逻辑问题还是随机性问题。
- 排查:
6.2 工具调用参数不符合预期
模型生成了工具调用请求,但参数格式错误或值不对。
可能原因1:参数Schema定义太宽松。
- 排查:JSON Schema定义是否足够严格?例如,一个要求是“数字”的参数,是否被定义成了
type: string? - 技巧:充分利用JSON Schema的约束。使用
enum来限定可选值,使用minimum/maximum限制数字范围。更严格的Schema能引导模型生成更准确的参数。
- 排查:JSON Schema定义是否足够严格?例如,一个要求是“数字”的参数,是否被定义成了
可能原因2:用户输入信息模糊,模型在“猜”。
- 场景:用户问“我的订单怎么样了?”,模型需要调用
get_order_status,但参数orderId未知。 - 解决方案:这不是工具的错,而是对话逻辑问题。你有两个选择:
- 在工具层处理:让工具函数能处理部分缺失的参数,例如查询当前会话用户的默认订单。但这可能增加工具复杂度。
- 在智能体指令层处理(推荐):在指令中明确要求智能体,当参数不足时,必须向用户追问。例如:“如果用户没有提供订单号,你必须先询问‘请问您的订单号是多少?’,在获得订单号之前,不要调用任何工具。”
- 场景:用户问“我的订单怎么样了?”,模型需要调用
6.3 处理复杂、多轮对话的状态管理
基础的agent.run()是单次调用。对于多轮对话,你需要自己管理对话历史(上下文)。
- 核心概念:将之前的对话历史(
{ role: 'user'/'model', parts: [...] })作为下一次run方法的输入的一部分。 - 实现模式:
class ConversationSession { private history: Array<{role: string, parts: any[]}> = []; async chat(userInput: string): Promise<string> { // 将用户输入加入历史 this.history.push({ role: 'user', parts: [{ text: userInput }] }); // 调用智能体,传入完整历史作为上下文 const response = await myAgent.run({ messages: this.history // 或者使用更简洁的 history 参数,取决于ADK的具体API }); // 将智能体回复加入历史 this.history.push({ role: 'model', parts: [{ text: response.text }] }); // 可选:限制历史长度,避免token超限 if (this.history.length > 20) { this.history = this.history.slice(-20); } return response.text; } } - 注意事项:上下文越长,消耗的Token越多,成本越高,且模型处理远端历史的能力会下降。需要根据场景设计合理的上下文窗口大小和摘要策略。
6.4 性能与成本优化
- 缓存:对于频繁查询且结果变化不频繁的工具(如产品信息),可以在工具函数内部实现缓存层(例如使用内存缓存
node-cache或Redis),避免重复调用外部API或查询数据库。 - 批量处理:如果业务场景允许,可以设计支持批量查询的工具,减少模型调用工具的频次。
- 选择合适的模型:对于简单的分类、路由、信息提取任务,使用
gemini-2.0-flash这类更小、更快的模型足以胜任,成本远低于gemini-2.5-pro。将最强大的模型用在最需要复杂推理的环节。
6.5 安全性与可靠性
- 工具权限:不是所有工具都应暴露给所有智能体。在设计多智能体系统时,应根据智能体的职责分配最小必要的工具集。
- 用户输入净化:在将用户输入传递给模型或工具之前,应进行基本的清理和验证,防止提示词注入攻击。
- 工具调用的副作用:对于执行写操作的工具(如创建订单、发送邮件),务必在工具内部实现严格的业务验证和确认机制。可以考虑让智能体先生成一个“待执行操作”的摘要,经用户确认后再触发实际执行。
ADK-JS将AI智能体开发从“魔术”变成了“工程”。它要求开发者投入更多前期设计,但回报是前所未有的可控性、可测试性和可维护性。当你习惯了用代码来定义智能体的每一个行为边界后,就很难再回到那个靠猜测和反复调整提示词来碰运气的时代了。
