在鸿蒙上跑 AI Agent:JiuwenClaw-on-OpenHarmony 完整实战

摘要:本文详细记录了如何将 AI Agent 框架 JiuwenClaw 移植到 HarmonyOS NEXT 上,实现一个具备 ReAct 推理循环、工具调用、持久化记忆、网页搜索、定时任务的端侧智能助手应用。全文包含架构设计、ArkTS 严格模式踩坑、核心代码实现及效果展示。

一、背景与动机

1.1 为什么要在鸿蒙上跑 AI Agent?

2026 年,AI Agent 已经从"聊天机器人"进化到了"能自主使用工具完成任务的智能体"。OpenClaw(GitHub 163K stars)、JiuwenClaw 等 Agent 框架风头正劲,但它们都运行在服务器/PC 上,依赖 Node.js + Docker,无法直接跑在移动端。

鸿蒙 HarmonyOS NEXT 作为全自研操作系统,拥有 ArkTS 声明式 UI、关系型数据库、Preferences 配置存储等完整能力。能不能把 AI Agent 搬到鸿蒙手机上?

答案是可以的。 本项目参考 nanoclaw-on-openharmony 的架构思路,结合 JiuwenClaw 的"懂你所想,自主演进"理念,实现了一个完整的端侧 AI Agent 应用。

1.2 JiuwenClaw 是什么?

JiuwenClaw 是"懂你所想,自主演进"的 Agent 框架,核心能力包括:

  • 任务规划:智能拆解复杂任务
  • 自演进:自动识别异常并优化技能
  • 上下文瘦身:保证长时运行不爆 token
  • 记忆随行:分层记忆,越用越懂你
  • Channel 接入:支持小艺、飞书等渠道

本项目是 JiuwenClaw 在鸿蒙设备上的轻量级实现。

二、架构设计

2.1 整体架构

┌──────────────────────────────────────────────┐

│ ArkUI 前端 │

│ ┌────────────┐ ┌────────────────┐ │

│ │ Index.ets │ │ SettingsPage │ │

│ │ 聊天界面 │ │ API/助手配置 │ │

│ └─────┬──────┘ └────────────────┘ │

│ │ │

│ ──────┼─────────── 服务层 ──────────────── │

│ │ │

│ ┌─────▼──────────────────────────────────┐ │

│ │ AgentCore.ets — ReAct 循环引擎 │ │

│ │ 用户输入 → ApiClient → DeepSeek API │ │

│ │ tool_calls? → ToolRegistry → 回 API │ │

│ │ stop? → 返回文本 → UI │ │

│ └────────────────────────────────────────┘ │

│ │ │ │ │

│ ┌────▼───┐ ┌───▼────┐ ┌───▼──────┐ │

│ │Database│ │Tool │ │Task │ │

│ │Service │ │Registry│ │Scheduler│ │

│ │SQLite │ │+ Tools │ │Cron/定时 │ │

│ └────────┘ └───┬────┘ └──────────┘ │

│ ┌─────┬───┴───┬──────┐ │

│ FileTools Memory WebTools TaskTools │

│ 文件读写 记忆存储 搜索/抓取 定时任务 │

└──────────────────────────────────────────────┘

2.2 技术选型

维度 方案
平台 HarmonyOS NEXT 5.0.5 (API 17)
语言 ArkTS(TypeScript 严格子集)
UI 框架 ArkUI 声明式
数据库 @ohos.data.relationalStore (SQLite)
配置 @ohos.data.preferences
网络 @ohos.net
.http
文件 @kit.CoreFileKit (fileIo)
LLM API DeepSeek Chat API(OpenAI 兼容格式)

2.3 文件结构

JiuwenClaw/entry/src/main/ets/

├── common/

│ ├── Types.ets # 全局类型定义(17个class)

│ ├── TextFormatter.ets # 文本处理工具集

│ └── Logger.ets # hilog 统一封装

├── services/

│ ├── AgentCore.ets # ⭐ 核心:ReAct 循环引擎

│ ├── ApiClient.ets # DeepSeek HTTP 客户端

│ ├── DatabaseService.ets# SQLite 数据库服务(5张表)

│ ├── ConfigService.ets # Preferences 配置管理

│ ├── ToolRegistry.ets # 工具注册中心

│ └── TaskScheduler.ets # 定时任务调度器(含 cron 解析)

├── tools/

│ ├── FileTools.ets # 文件读写列表

│ ├── MemoryTools.ets # 持久化记忆

│ ├── WebTools.ets # 网页抓取 + DuckDuckGo 搜索

│ └── TaskTools.ets # 定时任务 CRUD

├── pages/

│ ├── Index.ets # 聊天主界面

│ └── SettingsPage.ets # 设置页面

└── entryability/

└── EntryAbility.ets # 应用生命周期

三、核心实现

3.1 ReAct 循环——Agent 的大脑

ReAct(Reason + Act)是 AI Agent 的核心推理模式:LLM 思考后决定是否调用工具,拿到工具结果后继续思考,直到给出最终回答。

用户输入

构造 system prompt + 对话历史

┌─→ 调用 DeepSeek API

│ ↓

│ finish_reason == “tool_calls”?

│ ├─ Yes → 执行工具 → 把结果喂回 API ─┐

│ └─ No → 返回文本 → 显示在 UI │

│ │

└────────────────────────────────────────┘

(最多 15 次迭代,防止无限循环)

核心代码(AgentCore.ets):

async runAgent(

userMessage: string,

chatId: string,

onEvent: (event: AgentEvent) => void

): Promise {

// 1. 保存用户消息到 DB

// 2. 加载最近 50 条对话历史

// 3. 构造 API messages(system + history)

for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {

const response = await apiClient.sendMessage(config, messages, tools);

const choice = response.choices[0];

const choiceToolCalls = choice.message.toolCalls;

if (choiceToolCalls !== null && choiceToolCalls.length > 0) {

// 有工具调用 → 逐个执行 → 结果追加到 messages → 继续循环

for (let j = 0; j < choiceToolCalls.length; j++) {

const tc = choiceToolCalls[j];

const result = await toolRegistry.executeTool(

tc.function_call.name,

tc.function_call.arguments,

tc.id

);

// 工具结果作为 tool message 追加

const toolResultMsg = new ApiMessage();

toolResultMsg.role = ‘tool’;

toolResultMsg.content = result.content;

toolResultMsg.toolCallId = tc.id;

messages.push(toolResultMsg);

}

continue; // 继续循环,让 LLM 根据工具结果决定下一步

}

// 没有工具调用 → 最终回复

onEvent({ type: ‘text’, text: choice.message.content });

return;

}

}

关键设计决策:

  1. 回调模式:ArkTS 对 generator/yield 支持有限,用 onEvent 回调代替 AsyncGenerator,UI 通过回调实时更新
  2. 对话窗口限制:只发送最近 50 条消息,防止 token 爆炸
  3. 错误透传:工具执行失败不中断循环,而是把错误信息返回给 LLM,让 LLM 自己决定怎么处理

3.2 工具系统——可插拔的能力扩展

工具注册与实现完全解耦。ToolRegistry 只负责路由和错误处理,不知道具体工具做什么:

class ToolRegistry {

private tools: Map<string, RegisteredTool> = new Map();

registerTool(name, description, schema, executor): void { … }

getToolDefinitions(): ToolDefinition[] { … } // 生成 OpenAI function calling 格式

executeTool(name, inputJson, toolCallId): Promise { … }

}

当前实现了 4 类共 10 个工具:

类别 工具 功能
文件操作 file_read / file_write / file_list 沙箱内文件读写
持久化记忆 memory_read / memory_write / memory_list 跨会话记忆
网络访问 web_fetch / web_search 网页抓取 + DuckDuckGo 搜索
定时任务 schedule_task / list_tasks / pause_task / resume_task / cancel_task Cron/定时任务管理

Schema 定义的 ArkTS 适配:由于 ArkTS 不允许对象字面量作为类型,工具的 JSON Schema 通过 JSON.parse() 构造:

const schema: Record<string, Object> = JSON.parse(

‘{“type”:“object”,“properties”:{“path”:{“type”:“string”,“description”:“File path”}},“required”:[“path”]}’

) as Record<string, Object>;

toolRegistry.registerTool(‘file_read’, ‘Read a file’, schema, async (params) => {

// 实现…

});

3.3 数据库设计

使用 @ohos.data.relationalStore 提供的 SQLite,5 张表覆盖所有持久化需求:

表名 用途 核心字段
chats 会话列表 id, name, last_message_time
messages 消息历史 chat_id, sender, content, tool_calls (JSON)
scheduled_tasks 定时任务 schedule_type, schedule_value, next_run, status
task_run_logs 任务执行日志 task_id, duration_ms, status, result
kv_store 键值存储 key, value

API 差异处理:Node.js 中 better-sqlite3 是同步 API,鸿蒙的 relationalStore 是全异步:

// NanoClaw (Node.js)

const rows = db.prepare(sql).all(args);

// JiuwenClaw (ArkTS)

const resultSet = await rdbStore.querySql(sql, args);

while (resultSet.goToNextRow()) {

// 逐行遍历 ResultSet

}

resultSet.close();

3.4 定时任务调度器

支持三种调度模式:

模式 示例 说明
cron 0 9 * * * 每天早上 9 点
interval 3600000 每小时
once 60000 1 分钟后执行一次

其中 cron 解析器是完全自写的轻量级实现(NanoClaw 依赖的 cron-parser NPM 包在鸿蒙上不可用),支持通配符 *、范围 1-5、步进 */5、列表 1,3,5

function matchCronField(expr: string, value: number, min: number, max: number): boolean {

if (expr === '') return true;

_ const commaParts = expr.split(‘,’);_

_ for (let ci = 0; ci < commaParts.length; ci++) {_

_ const part = commaParts[ci];_

_ if (part.indexOf(‘/’) >= 0) {_

_ // 步进逻辑:_/5 → 从 min 开始每隔 5 匹配

} else if (part.indexOf(‘-’) >= 0) {

// 范围逻辑:1-5 → 1到5之间匹配

} else {

// 精确值匹配

}

}

return false;

}

⚠️ PoC 限制:定时任务只在 App 前台运行时轮询(每 60 秒),后台不运行。生产环境可接入 @ohos.WorkSchedulerExtensionAbility 实现后台调度。

3.5 API 客户端

使用 @ohos.net.http 调用 DeepSeek API(OpenAI 兼容格式),关键特性:

  • 指数退避重试:429 限流和网络错误自动重试 3 次(2s → 4s → 8s)
  • 手动 JSON 构建:由于 ArkTS 不允许匿名对象字面量,请求体通过手动拼接 JSON 字符串构造
  • camelCase snake_case:内部全用 camelCase(ArkTS 惯例),请求时转 snake_case,响应时转回来

// 非流式请求

POST https://api.deepseek.com/v1/chat/completions

{

“model”: “deepseek-chat”,

“messages”: […],

“tools”: [{…}],

“max_tokens”: 8192,

“stream”: false

}

四、ArkTS 严格模式踩坑记录

这是本项目最大的工程挑战。ArkTS 虽然基于 TypeScript,但启用了大量严格检查规则,很多 TS 的常见写法都不允许。

4.1 不允许对象字面量作为类型 (arkts-no-obj-literals-as-types)

// ❌ 编译报错

interface ToolCall {

function: {

name: string; // 嵌套对象字面量不允许

arguments: string;

};

}

// ✅ 正确写法:拆成独立 class

class ToolCallFunction {

name: string = ‘’;

arguments: string = ‘’;

}

class ToolCall {

id: string = ‘’;

function_call: ToolCallFunction = new ToolCallFunction();

}

影响范围:Types.ets 中所有嵌套 interface 都要拆成独立 class,最终产出 17 个 class。

4.2 不允许匿名对象字面量 (arkts-no-untyped-obj-literals)

// ❌ 编译报错

const config = {

apiKey: ‘xxx’,

baseUrl: ‘https://…’,

};

// ✅ 正确写法:new class + 逐字段赋值

const config = new ApiConfig();

config.apiKey = ‘xxx’;

config.baseUrl = ‘https://…’;

影响范围:全局,几乎每个文件都用到了对象字面量。工具的 JSON Schema 定义改用 JSON.parse() 构造。

4.3 不允许解构赋值 (arkts-no-destruct-decls)

// ❌ 编译报错

const [range, step] = part.split(‘/’);

// ✅ 正确写法

const slashParts = part.split(‘/’);

const range = slashParts[0];

const step = slashParts[1];

4.4 不允许 Spread 运算符 (arkts-no-spread)

// ❌ 编译报错

this.messages = […this.messages, newMsg];

// ✅ 正确写法

this.messages = this.messages.concat([newMsg]);

4.5 不允许 as const (arkts-no-as-const)

// ❌ 编译报错

type: ‘function’ as const,

// ✅ 直接赋值字符串

tc.type = ‘function’;

4.6 Import 必须在文件顶部 (arkts-no-misplaced-imports)

// ❌ 编译报错(import 放在文件末尾)

export function registerFileTools() { … }

import { util } from ‘@kit.ArkTS’; // 不能放这里

// ✅ 所有 import 放在文件最开头

import { util } from ‘@kit.ArkTS’;

export function registerFileTools() { … }

4.7 踩坑总结

首次编译产生了 67 个 ArkTS 编译错误,全部属于以上 6 类。修复策略:

  1. 所有 interfaceclass,嵌套对象拆成独立 class
  2. 所有对象字面量改 new Class() + 逐字段赋值
  3. 解构改显式索引,spread 改 concat
  4. JSON Schema 用 JSON.parse() 从字符串构造

经验教训:如果从零开始写鸿蒙 ArkTS 项目,建议第一天就把 TypeScript 的"坏习惯"戒掉——不用对象字面量、不用解构、不用 spread。

五、页面从 Settings 返回后的状态刷新问题

问题描述

配置完 API Key 保存后返回主页,界面仍然停留在"欢迎页面"(显示"请先配置 API Key"),无法进入对话。

根因分析

hasApiKey 状态只在 aboutToAppear() 中检查了一次。ArkUI 的页面路由使用栈结构,从 SettingsPage router.back() 返回时,Index 页面是从路由栈恢复的,**aboutToAppear**** 不会重新执行**。

解决方案

增加 onPageShow() 生命周期回调——每次页面可见时都重新检查 API Key:

async onPageShow(): Promise {

this.hasApiKey = await configService.hasApiKey();

}

这是 ArkUI 页面生命周期的一个常见坑:aboutToAppearonCreate(只执行一次),onPageShowonResume(每次可见都执行)。

六、UI 与功能升级

在 v2 版本中,对 UI 和交互进行了全面升级,更接近商业 App 体验:

6.1 视觉升级

  • 主题色:采用紫色渐变主题(#6C5CE7#A29BFE),科技感更强
  • 气泡卡片:用户消息使用渐变色,助手消息使用白色卡片带阴影,层次感鲜明
  • 头像标识:助手默认 🐾 头像,用户 👤 头像,圆形背景,视觉统一
  • 全局背景:浅紫灰 #F8F9FE 背景,比纯白色更护眼
  • 卡片式分组:设置页 3 张卡片(API/助手/RAG),信息分层清晰

6.2 交互升级

  • 快捷指令面板:点击输入栏左侧 ⚡ 按钮,展开 6 个预设操作:
    • 🧹 清空对话
    • 🧠 查看记忆
    • 📁 文件列表
    • ⏰ 定时任务
    • 🔍 搜索新闻
    • 📝 创建笔记
  • 工具详情展开:点击消息上方的工具标签(如"🔧 file_write"),可展开/折叠工具调用的 JSON 详情
  • 长按复制:所有消息气泡支持长按复制文本
  • 空态页:首次进入对话时,显示快捷示例按钮,引导用户操作
  • 参数可视化调节:设置页通过滑块调节 Temperature(0-2.0)和 Max Tokens(1k-32k),比手动输入更直观

6.3 体验增强

  • 消息计数:标题栏实时显示当前对话的消息条数
  • 运行状态:标题栏在 Agent 回复时显示"正在回复…“,空闲时显示"懂你所想,自主演进”
  • 连接状态指示:设置页标题栏显示 ● 已连接 / ● 未连接,API 连通状态一目了然
  • 增强系统提示词:增加更明确的工具使用指导、记忆引导和 personality 设定,助手回复更符合预期
  • Personality Friendly, competent, slightly playful. Like a smart friend who happens to have superpowers. Not corporate. Not sycophantic. Just genuinely helpful.

七、效果展示

基础对话

你:你好,介绍一下你自己

Agent:我是 JiuwenClaw,运行在你的鸿蒙设备上的 AI 助手…

文件操作

你:帮我创建一个 todo.md,写上今天要做的三件事

Agent:[🔧 Using: file_write]

Agent:我已经创建了 todo.md,内容如下:

  1. 完成工作
  2. 运动锻炼30分钟
  3. 读一会书…

记忆功能

你:记住我喜欢用中文回复

Agent:[🔧 Using: memory_write]

Agent:已保存到记忆中!以后我会默认用中文和你交流。

网页搜索(这里有点bug)

你:搜索一下鸿蒙最新动态

Agent:[🔧 Using: web_search]

Agent:根据搜索结果,鸿蒙最新动态包括…

定时任务

你:每天早上9点提醒我喝水

Agent:[🔧 Using: schedule_task]

Agent:任务已创建!每天早上 9:00 会提醒你喝水。(cron: 0 9 * * *)

快捷操作

你:点击 ⚡ 快捷按钮「清空对话」

Agent:[🧹 对话已清空,开始新的对话吧!]

八、与 NanoClaw 的对比

维度 NanoClaw (原版) JiuwenClaw (本项目)
运行时 Node.js 20+ ArkTS (ArkCompiler)
Agent 执行 Docker 容器 + Claude SDK 进程内 ReAct 循环
LLM 后端 Claude (Anthropic) DeepSeek(OpenAI 兼容)
IPC 方式 文件系统 IPC 直接函数调用
数据库 better-sqlite3(同步) relationalStore(异步)
安全模型 容器隔离 App 沙箱隔离
部署 服务器常驻进程 手机 App
代码量 ~3500 行 TS ~80KB ArkTS(17个文件)

八、当前限制与后续规划

当前限制

  • 非流式响应:使用 stream: false,回复生成后一次性显示
  • 仅前台调度:定时任务只在 App 前台轮询
  • 单会话:PoC 只有一个默认对话
  • 无浏览器自动化:鸿蒙上无 Chrome CDP

后续规划

  • SSE 流式响应,打字机效果
  • 接入 WorkSchedulerExtensionAbility 实现后台调度
  • 多会话管理
  • MCP 远程工具(高德地图等)
  • RAG 知识检索(SiliconFlow BGE-M3)
  • 接入鸿蒙小艺 Channel

九、总结

本项目证明了在鸿蒙设备上运行完整 AI Agent 是完全可行的。通过:

  1. 砍掉 Docker 依赖:用进程内 ReAct 循环替代容器化执行
  2. 适配 ArkTS 严格模式:系统性解决 67 个编译错误
  3. 利用鸿蒙原生能力:SQLite、Preferences、HTTP、FileIO 一应俱全

最终实现了一个能在手机上独立运行的 AI 助手,支持文件操作、网页搜索、记忆、定时任务等完整 Agent 能力。

端侧 AI Agent 是未来的趋势之一——数据不出设备、响应更快、隐私更好。期待更多开发者加入鸿蒙 AI 生态!


项目参考

开发环境:DevEco Studio + HarmonyOS NEXT 5.0.5 (API 17)

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐