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 整体架构分层解析

从源码结构来看,该项目通常包含以下几个核心层次:

  1. 技能协议层(Skill Protocol) :这是框架的基石。它定义了 BaseSkill ISkill 这样的抽象类或接口,规定了任何一个“技能”必须实现的方法,例如 execute(input: string, context?: SkillContext): Promise<SkillResult> SkillResult 则是一个标准化的结果容器,包含输出文本、状态码、可能的结构化数据等。这确保了所有技能对外表现一致。

  2. 模型适配层(Model Adapter) :这一层负责与具体的AI模型服务通信。它会有一个 BaseModelAdapter ,然后派生出 OpenAIAdapter AzureOpenAIAdapter CustomModelAdapter 等。适配器的职责是将框架内部统一的请求格式,转换为目标模型API所需的特定HTTP请求,并接收响应,将其反序列化为框架内部的统一格式。这里会处理API密钥、基础URL、模型名称等配置信息。

  3. 技能实现层(Skill Implementation) :这是开发者主要工作的层面。开发者创建具体的技能类,如 TranslationSkill SummarySkill ,它们继承自 BaseSkill 。在这些类里,开发者需要实现核心的 execute 方法,该方法内部通常会做三件事:

    • 输入预处理 :对用户输入进行清洗、校验或格式化。
    • 构造Prompt :根据技能目标,编写或组装发送给模型的提示词。这是决定技能效果的关键。
    • 调用适配器并解析输出 :通过依赖注入或工厂模式获取对应的模型适配器实例,发起调用,并对返回的文本进行解析,提取所需信息,封装成 SkillResult
  4. 运行时与管理层(Runtime & Manager) :框架可能会提供一个 SkillManager SkillEngine 的单例或服务。它的职责是管理所有已注册技能的声明周期,提供技能发现(如根据技能ID查找)、技能执行调度、以及统一的错误处理和日志记录。在鸿蒙环境下,它还需要确保异步调用与UI更新的正确协作。

  5. 工具与工具调用层(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 。它需要用户输入一个城市名称。技能内部会做两件事:

  1. 调用第三方天气API(如和风天气、OpenWeatherMap)获取该城市的结构化天气数据。
  2. 将结构化数据连同一条系统提示词(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
      };
    }
  }
}

关键步骤解析:

  1. 参数验证 :这是保证技能健壮性的第一步。我们同时支持字符串输入和对象输入,提高了易用性。
  2. 外部服务调用 WeatherApiService 是一个封装了具体天气API调用的服务类。这里将其分离,符合单一职责原则。在实际项目中,你需要申请相应天气服务的API Key并实现其调用逻辑。
  3. Prompt工程 :这是AI技能的核心。我们使用了“系统提示”来设定AI的角色和任务风格,“用户提示”则提供了具体的结构化数据。好的Prompt能极大提升输出质量。这里只是一个简单示例,你可以设计更复杂、更个性化的Prompt。
  4. 结果封装 :我们将原始天气数据和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)

这是更高级的模式。你需要:

  1. 定义工具列表 :将每个技能的 description parametersSchema 按照OpenAI的工具调用格式进行封装。
  2. 在对话请求中注入工具描述 :调用AI模型时,将工具列表作为 tools 参数传入。
  3. 解析模型的工具调用请求 :如果模型返回的 finish_reason tool_calls ,则解析出它想要调用的工具名称和参数。
  4. 路由并执行对应技能 :根据工具名称找到对应的技能,传入解析好的参数并执行。
  5. 将技能执行结果返回给模型 :将技能返回的 SkillResult.data 作为该工具调用的结果,再次发送给模型,让模型生成面向用户的最终回答。

这要求模型适配器层具备处理工具调用流的能力。实现这一功能后,你的鸿蒙应用就能与用户进行真正的“智能对话”,AI可以自主决定在何时调用哪个技能来完成任务,体验将大大提升。

6. 开发与部署中的常见问题与排查

在实际开发基于此类框架的应用时,你可能会遇到一些典型问题。

6.1 网络与权限问题

  • 问题 :应用无法发起网络请求,或请求被拒绝。
  • 排查
    1. 检查 module.json5 文件是否已声明网络权限: "requestPermissions": [{"name": "ohos.permission.INTERNET"}]
    2. 确认设备或模拟器已开启网络连接。
    3. 如果使用第三方API且其服务器在国内无法直接访问,你需要确保你的请求地址是可达的。 (此处严格遵守安全要求,不展开任何相关讨论)
    4. 使用鸿蒙的 @ohos.net.http 模块进行调试,先尝试一个简单的GET请求到公共API(如 httpbin.org/get ),确认基础网络层是通的。

6.2 API密钥与配置管理

  • 问题 401 Unauthorized 403 Forbidden 错误。
  • 排查
    1. 绝对不要 将API密钥提交到Git等版本控制系统。使用 .env 文件(通过 @ohos/node-api 或其他配置库读取)或鸿蒙的 Preferences / Configuration 能力来管理敏感配置。
    2. 检查密钥是否拼写正确,是否已过期,是否绑定了正确的IP或域名限制。
    3. 检查请求头中的 Authorization 等鉴权字段格式是否正确。

6.3 异步操作与UI线程阻塞

  • 问题 :应用在执行技能时界面卡死或无响应。
  • 排查
    1. 确保所有网络请求和耗时操作(如大量文本处理)都放在 Promise async 函数或 TaskPool (鸿蒙后台任务池)中执行, 不要 在UI线程中同步执行。
    2. 在技能执行前后,正确使用 @State 变量控制加载状态和按钮禁用状态,给用户明确的反馈。
    3. 考虑为长时间运行的任务(如下载大模型、处理长文档)添加取消机制。

6.4 技能执行超时或性能不佳

  • 问题 :技能调用时间过长,用户体验差。
  • 排查与优化
    1. 设置超时 :在模型适配器的HTTP请求和技能本身的 execute 方法中设置合理的超时时间(如30秒)。
    2. 优化Prompt :过长的Prompt会增加Token消耗和响应时间。尽量精简系统提示,保持用户输入简洁。
    3. 缓存策略 :对于结果变化不频繁的技能(如某些知识查询、天气数据可缓存短期),可以在技能管理器或技能内部实现结果缓存,避免重复调用外部服务。
    4. 模型选择 :如果不需要很强的创造力,可以尝试使用更小、更快的模型(如 gpt-3.5-turbo 而非 gpt-4 ),并调整 temperature 等参数。

6.5 错误处理与用户提示

  • 问题 :技能内部出错时,用户只看到“操作失败”等模糊提示。
  • 最佳实践
    1. 分层错误码 :在 SkillResult 中定义清晰的错误码体系,如 1001 代表输入无效, 2001 代表网络错误, 3001 代表第三方服务错误等。
    2. 友好错误信息 :在返回给UI层的错误 message 中,提供对用户友好且可操作的提示,例如“城市名称不能为空,请重新输入”,而不是“参数校验失败”。
    3. 详细日志 :在开发阶段,在技能执行的关键节点和捕获异常时,使用 console.debug console.error 输出详细信息,便于排查。生产环境可接入更专业的日志服务。

通过系统性地理解“harmonyos-ai-skill”这类框架的设计理念,并亲手实践构建一个完整的技能,你不仅能快速为鸿蒙应用添加AI能力,更能深入掌握AI与原生应用融合的架构模式。这其中的关键,在于理解“抽象”与“适配”的思想,将复杂多变的AI服务封装成稳定统一的内部接口,从而让应用开发聚焦于创造价值本身。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐