鸿蒙AI技能开发框架:构建标准化AI Agent的架构与实践
在AI应用开发中,如何高效、标准化地集成大语言模型等AI能力是开发者面临的核心挑战。其原理在于通过抽象层和适配器模式,将异构的AI服务API统一为内部可调用的接口,从而屏蔽底层差异。这种架构设计的技术价值在于显著降低了开发复杂度,提升了代码复用性和可维护性,是实现AI Agent或技能化AI功能的关键基础设施。典型的应用场景包括为移动应用快速集成智能对话、内容生成、数据分析等AI功能。本文聚焦的“
1. 项目概述:一个面向鸿蒙生态的AI技能开发框架
最近在鸿蒙生态的开发者社区里,看到不少朋友在讨论如何将AI能力,特别是大语言模型(LLM)的能力,便捷地集成到自己的鸿蒙应用里。这确实是个痛点,AI能力调用本身涉及网络请求、模型接口适配、结果解析等一系列繁琐工作,如果每个应用都从头写一遍,不仅效率低,代码也容易变得臃肿且难以维护。恰好,我关注到了一个名为“harmonyos-ai-skill”的开源项目,它正是为了解决这个问题而生。简单来说,这是一个为鸿蒙(HarmonyOS)应用开发者设计的AI技能(或称为AI Agent)开发框架,旨在将复杂的AI模型调用抽象成简单、统一、可复用的“技能”,让开发者能像搭积木一样,快速为自己的应用注入AI智能。
这个项目由开发者DengShiyingA创建并维护,其核心价值在于“降本增效”。它试图屏蔽掉不同AI服务提供商(如OpenAI、国内各大模型厂商)在API调用方式、参数格式、返回结果解析上的差异,为鸿蒙开发者提供一个标准化的接入层。你可以把它理解为一个“AI能力中间件”或“AI技能市场的基础设施”。对于正在探索鸿蒙原生应用开发,尤其是希望融入AI对话、内容生成、智能分析等功能的开发者而言,这个项目提供了一个非常值得研究的起点和工具箱。它适合有一定鸿蒙应用开发基础(熟悉ArkTS/ArkUI),并且对集成第三方AI服务有需求的开发者。即使你只是对AI应用架构感兴趣,这个项目的设计思路也颇具参考价值。
2. 核心设计思路与架构拆解
2.1 核心问题与设计目标
在鸿蒙应用中集成AI,开发者通常会面临几个典型问题。首先是 接口异构性 :不同AI模型的HTTP API端点、请求头(尤其是鉴权)、请求体格式(JSON结构)各不相同。其次是 异步处理与状态管理 :AI调用是网络IO密集型操作,需要在鸿蒙的UI线程外妥善处理,并优雅地更新界面状态。再者是 技能抽象 :一个“翻译”技能或“总结”技能,其内部可能调用同一个模型,但输入输出处理和提示词(Prompt)工程完全不同,如何将它们模块化?最后是 生态适配 :如何让这些技能方便地在鸿蒙应用的生命周期、权限系统、线程模型下工作?
“harmonyos-ai-skill”框架的设计目标,正是为了系统性地解决上述问题。它的核心思路是**“定义协议,实现适配,提供运行时”**。框架定义了一套标准的“技能”接口协议,任何符合该协议的模块都可以被框架管理和调用。同时,框架提供了对接不同AI模型后端(如OpenAI格式的API)的适配器(Adapter)。开发者只需关注技能本身的业务逻辑(输入处理、Prompt构建、输出解析),而无需关心底层是调用了哪个模型、网络请求如何发送。这种设计极大地提升了代码的复用性和可维护性。
2.2 整体架构分层解析
从源码结构来看,该项目通常包含以下几个核心层次:
-
技能协议层(Skill Protocol) :这是框架的基石。它定义了
BaseSkill或ISkill这样的抽象类或接口,规定了任何一个“技能”必须实现的方法,例如execute(input: string, context?: SkillContext): Promise<SkillResult>。SkillResult则是一个标准化的结果容器,包含输出文本、状态码、可能的结构化数据等。这确保了所有技能对外表现一致。 -
模型适配层(Model Adapter) :这一层负责与具体的AI模型服务通信。它会有一个
BaseModelAdapter,然后派生出OpenAIAdapter、AzureOpenAIAdapter或CustomModelAdapter等。适配器的职责是将框架内部统一的请求格式,转换为目标模型API所需的特定HTTP请求,并接收响应,将其反序列化为框架内部的统一格式。这里会处理API密钥、基础URL、模型名称等配置信息。 -
技能实现层(Skill Implementation) :这是开发者主要工作的层面。开发者创建具体的技能类,如
TranslationSkill、SummarySkill,它们继承自BaseSkill。在这些类里,开发者需要实现核心的execute方法,该方法内部通常会做三件事:- 输入预处理 :对用户输入进行清洗、校验或格式化。
- 构造Prompt :根据技能目标,编写或组装发送给模型的提示词。这是决定技能效果的关键。
- 调用适配器并解析输出 :通过依赖注入或工厂模式获取对应的模型适配器实例,发起调用,并对返回的文本进行解析,提取所需信息,封装成
SkillResult。
-
运行时与管理层(Runtime & Manager) :框架可能会提供一个
SkillManager或SkillEngine的单例或服务。它的职责是管理所有已注册技能的声明周期,提供技能发现(如根据技能ID查找)、技能执行调度、以及统一的错误处理和日志记录。在鸿蒙环境下,它还需要确保异步调用与UI更新的正确协作。 -
工具与工具调用层(Tools & Function Calling) :这是进阶能力。高级的AI技能框架会支持“工具调用”(Function Calling),即让大模型在对话中决定何时调用某个外部工具(技能)。框架需要定义一套工具描述规范,并在模型适配层支持将工具描述注入到对话上下文中,同时能解析模型的工具调用请求,并路由到对应的技能去执行。这能实现更复杂、自主的AI智能体行为。
注意 :以上架构分析是基于同类项目(如LangChain for HarmonyOS)的通用设计模式进行的合理推演。具体到“harmonyos-ai-skill”项目,其实现细节可能有所不同,但核心的分层思想和组件职责是相通的。阅读源码时,应重点理解各模块如何遵循“依赖倒置”和“单一职责”原则。
3. 关键模块深度解析与实操要点
3.1 技能(Skill)基类的设计与实现
一个健壮的技能基类是框架的骨架。我们来看看一个典型的 BaseSkill 抽象类应该包含哪些要素。
// 示例代码,基于ArkTS语法推演
export interface SkillContext {
sessionId?: string; // 会话ID,用于关联多轮对话
extraParams?: Record<string, any>; // 额外上下文参数
}
export interface SkillResult {
success: boolean;
code: number; // 自定义状态码,如0成功,非0失败
message: string; // 结果信息或错误信息
data?: any; // 技能输出的主要数据,可以是字符串或复杂对象
usage?: { // 可选:记录本次调用的token消耗等信息
promptTokens?: number;
completionTokens?: number;
};
}
export abstract class BaseSkill {
// 技能唯一标识符,用于在管理器中注册和查找
abstract readonly id: string;
// 技能名称,用于展示
abstract readonly name: string;
// 技能描述,可用于自动生成工具调用描述
abstract readonly description: string;
// 技能所需参数的模式定义(JSON Schema),用于动态UI生成和验证
abstract readonly parametersSchema?: object;
// 核心执行方法
abstract execute(input: string | object, context?: SkillContext): Promise<SkillResult>;
// 可选:技能初始化方法,用于加载资源等
async initialize(): Promise<void> {
// 默认空实现
}
// 可选:技能清理方法
async dispose(): Promise<void> {
// 默认空实现
}
}
实操要点与心得:
- 输入泛型 :
execute方法的input参数类型设计为string | object是很有必要的。简单技能可能只需要文本输入,但复杂技能(如数据分析)可能需要结构化的JSON数据。在实现具体技能时,需要在方法开头对输入进行类型检查和转换。 - 上下文传递 :
SkillContext的设计至关重要。它使得技能在执行时能获取到会话状态、用户偏好等全局信息,是实现多轮对话和个性化响应的基础。例如,翻译技能可以通过context知道用户之前选择的源语言和目标语言。 - 结果标准化 :统一的
SkillResult格式让上层调用者处理起来非常方便。无论底层技能是成功还是失败,调用者都通过检查success和code字段来判断,并通过data获取结果。这避免了到处写try-catch处理不同风格的错误。 - 参数模式(Schema) :
parametersSchema是支持动态技能调用的关键。如果你希望构建一个能自动发现并调用技能的AI助手,AI模型需要知道每个技能需要什么参数。通过JSON Schema描述,模型可以生成结构化的参数来调用技能。这在实现“工具调用”功能时是必不可少的。
3.2 模型适配器(Model Adapter)的封装艺术
模型适配器是框架与外界AI服务的桥梁。它的核心职责是“转换”。下面以对接OpenAI兼容API为例,展示一个适配器的核心结构。
// 示例代码
export interface ModelRequest {
model: string; // 模型名称,如 'gpt-3.5-turbo'
messages: Array<{role: 'system' | 'user' | 'assistant'; content: string}>;
temperature?: number;
max_tokens?: number;
// ... 其他OpenAI兼容参数
}
export interface ModelResponse {
id: string;
choices: Array<{
message: {
role: string;
content: string;
};
finish_reason: string;
}>;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
export class OpenAIAdapter {
private baseURL: string;
private apiKey: string;
private httpClient: HttpClient; // 鸿蒙的网络请求客户端
constructor(config: {baseURL: string; apiKey: string}) {
this.baseURL = config.baseURL;
this.apiKey = config.apiKey;
this.httpClient = ... // 初始化鸿蒙的httpClient
}
async createChatCompletion(request: ModelRequest): Promise<ModelResponse> {
const url = `${this.baseURL}/v1/chat/completions`;
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
};
try {
// 使用鸿蒙的@ohos.net.http模块发起请求
let httpRequest = http.createHttp();
let options: http.HttpRequestOptions = {
method: http.RequestMethod.POST,
header: headers,
extraData: JSON.stringify(request)
};
let response = await httpRequest.request(url, options);
if (response.responseCode === 200) {
let result = JSON.parse(response.result as string) as ModelResponse;
return result;
} else {
// 处理HTTP错误,抛出统一的业务异常
throw new Error(`API请求失败: ${response.responseCode}, ${response.result}`);
}
} catch (error) {
// 处理网络异常等
throw new Error(`网络或解析错误: ${error.message}`);
} finally {
// 释放资源
httpRequest.destroy();
}
}
// 可以添加其他方法,如流式响应支持、函数调用处理等
}
注意事项与避坑指南:
- 网络模块选择 :鸿蒙应用开发中,网络请求应使用官方的
@ohos.net.http模块。务必在模块的module.json5文件中声明ohos.permission.INTERNET网络权限。 - 异步与异常处理 :所有网络操作都是异步的,必须使用
async/await或Promise。异常处理要分层:网络层异常、API业务层异常(如额度不足、模型不存在)、响应解析异常。框架应定义一套内部错误类型,将底层异常转化为统一的SkillResult错误格式。 - 配置管理 :API Key和Base URL是敏感配置。 绝对不要 硬编码在代码中。应该通过鸿蒙的
@ohos.app.ability.Configuration或安全的外部配置服务来获取。在开源项目中,通常会要求用户通过配置文件或环境变量来设置。 - 超时与重试 :生产环境必须配置请求超时。对于可重试的错误(如网络抖动、服务器5xx错误),适配器应实现简单的重试机制,并配合退避策略(如指数退避),以提升鲁棒性。
- 流式支持 :如果希望支持类似ChatGPT的打字机效果,需要处理服务器发送事件(Server-Sent Events, SSE)。这要求适配器能处理分块传输的响应,并逐步将解析出的文本片段通过回调或事件机制传递给上层。这部分实现复杂度会显著增加。
3.3 技能管理器的核心作用
技能管理器( SkillManager )是框架的门面(Facade),它简化了技能的使用。其核心功能包括注册、发现和执行。
// 示例代码
export class SkillManager {
private skillRegistry: Map<string, BaseSkill> = new Map();
private adapterRegistry: Map<string, BaseModelAdapter> = new Map();
// 单例模式确保全局唯一
private static instance: SkillManager;
static getInstance(): SkillManager {
if (!SkillManager.instance) {
SkillManager.instance = new SkillManager();
}
return SkillManager.instance;
}
private constructor() {}
// 注册技能
registerSkill(skill: BaseSkill): void {
if (this.skillRegistry.has(skill.id)) {
console.warn(`技能 ${skill.id} 已存在,将被覆盖`);
}
this.skillRegistry.set(skill.id, skill);
console.log(`技能注册成功: ${skill.name} (${skill.id})`);
}
// 注册模型适配器
registerAdapter(name: string, adapter: BaseModelAdapter): void {
this.adapterRegistry.set(name, adapter);
}
// 根据ID获取技能
getSkill(skillId: string): BaseSkill | undefined {
return this.skillRegistry.get(skillId);
}
// 执行技能(简化版)
async executeSkill(skillId: string, input: string, context?: SkillContext): Promise<SkillResult> {
const skill = this.getSkill(skillId);
if (!skill) {
return {
success: false,
code: 404,
message: `未找到技能: ${skillId}`
};
}
try {
// 这里可以加入执行前钩子(如日志、权限检查)
console.log(`开始执行技能: ${skillId}`);
const result = await skill.execute(input, context);
// 这里可以加入执行后钩子(如结果缓存、使用量统计)
console.log(`技能执行完毕: ${skillId}, 状态: ${result.success}`);
return result;
} catch (error) {
console.error(`技能执行异常 ${skillId}:`, error);
return {
success: false,
code: 500,
message: `技能执行内部错误: ${error.message}`,
data: null
};
}
}
// 获取所有技能列表(可用于UI展示)
getAllSkills(): BaseSkill[] {
return Array.from(this.skillRegistry.values());
}
}
设计心得:
- 依赖注入 :上面的简化版
SkillManager直接让技能内部处理适配器。更优雅的设计是采用依赖注入(DI),在注册技能时,由管理器或一个专门的工厂来将配置好的适配器实例注入到技能中。这样技能类与具体适配器解耦,更易于测试和替换。 - 生命周期管理 :管理器可以在
executeSkill前后提供钩子函数,方便实现AOP(面向切面编程)功能,如统一的性能监控、日志记录、权限校验、使用限流等。这对于构建企业级应用非常重要。 - 技能发现与元数据 :
getAllSkills()方法返回的技能列表,包含了每个技能的id,name,description,parametersSchema等元数据。这些信息可以直接用来在应用UI上动态生成一个“技能市场”或“技能面板”,用户可以看到所有可用技能及其描述,甚至能根据Schema动态生成输入表单。
4. 实战:从零构建一个天气查询AI技能
现在,我们结合一个具体场景,演示如何使用(或参考)“harmonyos-ai-skill”框架的思路,构建一个实用的“天气查询”技能。这个技能本身不直接调用大模型生成天气,而是先调用一个真实的天气API获取数据,然后让大模型以更自然、更个性化的语言来播报天气。这体现了“AI作为解释器和呈现层”的混合模式。
4.1 技能定义与设计
我们的技能ID定为 weather_query 。它需要用户输入一个城市名称。技能内部会做两件事:
- 调用第三方天气API(如和风天气、OpenWeatherMap)获取该城市的结构化天气数据。
- 将结构化数据连同一条系统提示词(Prompt)发送给大模型,让模型生成一段友好的天气播报文本。
因此,这个技能的 execute 方法输入是城市名(字符串),输出是生成的播报文本。
4.2 具体实现步骤
首先,我们需要实现技能类 WeatherQuerySkill 。
// WeatherQuerySkill.ets
import { BaseSkill, SkillContext, SkillResult } from './BaseSkill';
import { WeatherApiService } from './WeatherApiService'; // 假设的天气API服务
import { OpenAIAdapter } from './OpenAIAdapter'; // 模型适配器
export class WeatherQuerySkill extends BaseSkill {
readonly id = 'weather_query';
readonly name = '天气查询助手';
readonly description = '查询指定城市的当前天气,并以生动的方式播报。';
readonly parametersSchema = {
type: 'object',
properties: {
city: {
type: 'string',
description: '要查询天气的城市名称,例如:北京、上海、New York'
}
},
required: ['city']
};
private weatherApi: WeatherApiService;
private aiAdapter: OpenAIAdapter;
// 通过构造函数注入依赖
constructor(weatherApi: WeatherApiService, aiAdapter: OpenAIAdapter) {
super();
this.weatherApi = weatherApi;
this.aiAdapter = aiAdapter;
}
async execute(input: string | object, context?: SkillContext): Promise<SkillResult> {
// 1. 参数解析与验证
let cityName: string;
if (typeof input === 'string') {
cityName = input.trim();
} else if (input && typeof input === 'object' && input['city']) {
cityName = String(input['city']).trim();
} else {
return {
success: false,
code: 400,
message: '输入参数错误,请输入城市名称或包含city字段的对象。'
};
}
if (!cityName) {
return { success: false, code: 400, message: '城市名称不能为空。' };
}
try {
// 2. 调用真实天气API
console.log(`正在查询 ${cityName} 的天气...`);
const weatherData = await this.weatherApi.getCurrentWeather(cityName);
// 假设weatherData返回格式:{ temp: 22, condition: '晴', humidity: 65, windSpeed: 10, ... }
// 3. 构建给AI的Prompt,将数据转化为自然语言
const systemPrompt = `你是一个专业的天气播报员,请根据提供的结构化天气数据,生成一段亲切、简洁的天气播报。可以适当加入穿衣建议或出行提醒。请直接输出播报文本,不要提及“数据如下”等前缀。`;
const userPrompt = `城市:${cityName}\n天气数据:温度 ${weatherData.temp}°C,天气状况 ${weatherData.condition},湿度 ${weatherData.humidity}%,风速 ${weatherData.windSpeed} km/h。`;
const modelRequest = {
model: 'gpt-3.5-turbo',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt }
],
temperature: 0.7, // 适当创造性
max_tokens: 150
};
// 4. 调用AI模型生成播报
console.log(`请求AI生成天气播报...`);
const aiResponse = await this.aiAdapter.createChatCompletion(modelRequest);
const aiGeneratedText = aiResponse.choices[0]?.message?.content?.trim() || '未能生成天气播报。';
// 5. 返回成功结果
return {
success: true,
code: 0,
message: '天气查询成功',
data: {
rawData: weatherData, // 保留原始数据,供需要者使用
report: aiGeneratedText // AI生成的播报文本
},
usage: aiResponse.usage // 传递token使用情况
};
} catch (error) {
// 错误处理
console.error(`天气查询技能执行失败:`, error);
let errorCode = 500;
let errorMessage = `技能执行失败: ${error.message}`;
// 可以根据error类型细化错误码和信息,例如区分网络错误、API错误、AI服务错误
if (error.message.includes('城市不存在')) {
errorCode = 404;
errorMessage = `未找到城市: ${cityName}`;
}
return {
success: false,
code: errorCode,
message: errorMessage
};
}
}
}
关键步骤解析:
- 参数验证 :这是保证技能健壮性的第一步。我们同时支持字符串输入和对象输入,提高了易用性。
- 外部服务调用 :
WeatherApiService是一个封装了具体天气API调用的服务类。这里将其分离,符合单一职责原则。在实际项目中,你需要申请相应天气服务的API Key并实现其调用逻辑。 - Prompt工程 :这是AI技能的核心。我们使用了“系统提示”来设定AI的角色和任务风格,“用户提示”则提供了具体的结构化数据。好的Prompt能极大提升输出质量。这里只是一个简单示例,你可以设计更复杂、更个性化的Prompt。
- 结果封装 :我们将原始天气数据和AI生成的文本一同返回。这样上层应用既可以直接展示生动的播报,也可以在需要时使用原始数据绘制图表。
4.3 在鸿蒙应用中的集成与使用
最后,我们看看如何在鸿蒙应用的UI页面中集成并使用这个技能。
// Index.ets (部分代码)
import { SkillManager } from '../skills/SkillManager';
import { WeatherQuerySkill } from '../skills/WeatherQuerySkill';
import { WeatherApiService } from '../service/WeatherApiService';
import { OpenAIAdapter } from '../adapter/OpenAIAdapter';
@Entry
@Component
struct Index {
@State cityName: string = '北京';
@State weatherReport: string = '';
@State isQuerying: boolean = false;
// 在aboutToAppear或初始化时注册技能
aboutToAppear() {
const skillManager = SkillManager.getInstance();
// 初始化依赖(实际项目中应使用依赖注入容器管理)
const weatherApi = new WeatherApiService('YOUR_WEATHER_API_KEY');
const aiAdapter = new OpenAIAdapter({
baseURL: 'https://api.openai.com/v1', // 或你的代理地址
apiKey: 'YOUR_OPENAI_API_KEY'
});
// 创建并注册技能
const weatherSkill = new WeatherQuerySkill(weatherApi, aiAdapter);
skillManager.registerSkill(weatherSkill);
console.info('天气查询技能已注册');
}
// 按钮点击事件处理函数
async onQueryWeather() {
if (this.isQuerying || !this.cityName.trim()) {
return;
}
this.isQuerying = true;
this.weatherReport = '查询中...';
try {
const skillManager = SkillManager.getInstance();
const result = await skillManager.executeSkill('weather_query', this.cityName.trim());
if (result.success) {
// 成功,更新UI显示AI生成的播报
this.weatherReport = result.data.report;
} else {
// 失败,显示错误信息
this.weatherReport = `查询失败: ${result.message} (错误码: ${result.code})`;
}
} catch (error) {
this.weatherReport = `发生未知错误: ${error.message}`;
} finally {
this.isQuerying = false;
}
}
build() {
Column({ space: 20 }) {
Text('智能天气查询').fontSize(30).fontWeight(FontWeight.Bold)
TextInput({ placeholder: '请输入城市名', text: this.cityName })
.onChange((value) => {
this.cityName = value;
})
.width('90%')
.height(40)
.padding(10)
.border({ width: 1, color: Color.Grey })
Button(this.isQuerying ? '查询中...' : '查询天气')
.onClick(() => this.onQueryWeather())
.width('50%')
.enabled(!this.isQuerying && !!this.cityName.trim())
// 显示结果
Scroll() {
Text(this.weatherReport)
.fontSize(18)
.textAlign(TextAlign.Start)
.padding(15)
.backgroundColor(Color.White)
.borderRadius(10)
.width('90%')
}
.height(300)
.width('100%')
.margin({ top: 20 })
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#F1F3F5')
}
}
集成要点:
- 生命周期管理 :技能的初始化和注册(如依赖创建)建议放在页面的
aboutToAppear或应用启动阶段。注意,API密钥等敏感信息切勿硬编码,应从安全配置中读取。 - 异步UI更新 :
executeSkill是异步操作,必须使用async/await,并在操作前后设置加载状态(isQuerying),以提供良好的用户体验,防止用户重复点击。 - 错误处理与用户反馈 :对技能返回的
SkillResult进行成功与否的判断,并将友好的错误信息展示给用户。避免将底层异常直接抛给用户界面。
5. 进阶话题:技能编排与工具调用
当应用中有多个技能时,简单的单次调用可能无法满足复杂需求。例如,用户可能说:“总结一下今天北京和上海的天气对比。” 这需要先调用两次天气查询技能,再将结果交给总结技能。这就涉及到 技能编排(Orchestration) 。
5.1 实现简单的线性编排
我们可以创建一个高阶的“技能编排器”,或者直接实现一个 WeatherCompareSkill 。但更通用的方式是,利用框架支持“工具调用”的特性。
// 一个设想中的编排使用示例
async function handleComplexQuery(userQuery: string) {
const skillManager = SkillManager.getInstance();
// 假设我们有一个“规划器”技能,能分析用户意图并生成执行计划
const planResult = await skillManager.executeSkill('query_planner', userQuery);
// planResult.data 可能是一个JSON数组,如:[
// { skill: 'weather_query', input: '北京' },
// { skill: 'weather_query', input: '上海' },
// { skill: 'text_summary', input: { texts: [/*北京结果*/, /*上海结果*/], instruction: '对比' } }
// ]
const executionPlan = planResult.data;
const intermediateResults = [];
for (const step of executionPlan) {
const stepResult = await skillManager.executeSkill(step.skill, step.input);
if (!stepResult.success) {
// 处理步骤失败
break;
}
intermediateResults.push(stepResult.data);
}
// 最后,可能还需要一个步骤来整合所有中间结果
// ...
}
5.2 对接大模型的工具调用(Function Calling)
这是更高级的模式。你需要:
- 定义工具列表 :将每个技能的
description和parametersSchema按照OpenAI的工具调用格式进行封装。 - 在对话请求中注入工具描述 :调用AI模型时,将工具列表作为
tools参数传入。 - 解析模型的工具调用请求 :如果模型返回的
finish_reason是tool_calls,则解析出它想要调用的工具名称和参数。 - 路由并执行对应技能 :根据工具名称找到对应的技能,传入解析好的参数并执行。
- 将技能执行结果返回给模型 :将技能返回的
SkillResult.data作为该工具调用的结果,再次发送给模型,让模型生成面向用户的最终回答。
这要求模型适配器层具备处理工具调用流的能力。实现这一功能后,你的鸿蒙应用就能与用户进行真正的“智能对话”,AI可以自主决定在何时调用哪个技能来完成任务,体验将大大提升。
6. 开发与部署中的常见问题与排查
在实际开发基于此类框架的应用时,你可能会遇到一些典型问题。
6.1 网络与权限问题
- 问题 :应用无法发起网络请求,或请求被拒绝。
- 排查 :
- 检查
module.json5文件是否已声明网络权限:"requestPermissions": [{"name": "ohos.permission.INTERNET"}]。 - 确认设备或模拟器已开启网络连接。
- 如果使用第三方API且其服务器在国内无法直接访问,你需要确保你的请求地址是可达的。 (此处严格遵守安全要求,不展开任何相关讨论) 。
- 使用鸿蒙的
@ohos.net.http模块进行调试,先尝试一个简单的GET请求到公共API(如httpbin.org/get),确认基础网络层是通的。
- 检查
6.2 API密钥与配置管理
- 问题 :
401 Unauthorized或403 Forbidden错误。 - 排查 :
- 绝对不要 将API密钥提交到Git等版本控制系统。使用
.env文件(通过@ohos/node-api或其他配置库读取)或鸿蒙的Preferences/Configuration能力来管理敏感配置。 - 检查密钥是否拼写正确,是否已过期,是否绑定了正确的IP或域名限制。
- 检查请求头中的
Authorization等鉴权字段格式是否正确。
- 绝对不要 将API密钥提交到Git等版本控制系统。使用
6.3 异步操作与UI线程阻塞
- 问题 :应用在执行技能时界面卡死或无响应。
- 排查 :
- 确保所有网络请求和耗时操作(如大量文本处理)都放在
Promise、async函数或TaskPool(鸿蒙后台任务池)中执行, 不要 在UI线程中同步执行。 - 在技能执行前后,正确使用
@State变量控制加载状态和按钮禁用状态,给用户明确的反馈。 - 考虑为长时间运行的任务(如下载大模型、处理长文档)添加取消机制。
- 确保所有网络请求和耗时操作(如大量文本处理)都放在
6.4 技能执行超时或性能不佳
- 问题 :技能调用时间过长,用户体验差。
- 排查与优化 :
- 设置超时 :在模型适配器的HTTP请求和技能本身的
execute方法中设置合理的超时时间(如30秒)。 - 优化Prompt :过长的Prompt会增加Token消耗和响应时间。尽量精简系统提示,保持用户输入简洁。
- 缓存策略 :对于结果变化不频繁的技能(如某些知识查询、天气数据可缓存短期),可以在技能管理器或技能内部实现结果缓存,避免重复调用外部服务。
- 模型选择 :如果不需要很强的创造力,可以尝试使用更小、更快的模型(如
gpt-3.5-turbo而非gpt-4),并调整temperature等参数。
- 设置超时 :在模型适配器的HTTP请求和技能本身的
6.5 错误处理与用户提示
- 问题 :技能内部出错时,用户只看到“操作失败”等模糊提示。
- 最佳实践 :
- 分层错误码 :在
SkillResult中定义清晰的错误码体系,如1001代表输入无效,2001代表网络错误,3001代表第三方服务错误等。 - 友好错误信息 :在返回给UI层的错误
message中,提供对用户友好且可操作的提示,例如“城市名称不能为空,请重新输入”,而不是“参数校验失败”。 - 详细日志 :在开发阶段,在技能执行的关键节点和捕获异常时,使用
console.debug或console.error输出详细信息,便于排查。生产环境可接入更专业的日志服务。
- 分层错误码 :在
通过系统性地理解“harmonyos-ai-skill”这类框架的设计理念,并亲手实践构建一个完整的技能,你不仅能快速为鸿蒙应用添加AI能力,更能深入掌握AI与原生应用融合的架构模式。这其中的关键,在于理解“抽象”与“适配”的思想,将复杂多变的AI服务封装成稳定统一的内部接口,从而让应用开发聚焦于创造价值本身。
更多推荐




所有评论(0)