在鸿蒙上跑 AI Agent:JiuwenClaw-on-OpenHarmony 完整实战
首次编译产生了67 个 ArkTS 编译错误,全部属于以上 6 类。所有interface改class,嵌套对象拆成独立 class所有对象字面量改+ 逐字段赋值解构改显式索引,spread 改concatJSON Schema 用从字符串构造经验教训:如果从零开始写鸿蒙 ArkTS 项目,建议第一天就把 TypeScript 的"坏习惯"戒掉——不用对象字面量、不用解构、不用 spread。本项
在鸿蒙上跑 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;
}
}
关键设计决策:
- 回调模式:ArkTS 对 generator/yield 支持有限,用
onEvent回调代替 AsyncGenerator,UI 通过回调实时更新 - 对话窗口限制:只发送最近 50 条消息,防止 token 爆炸
- 错误透传:工具执行失败不中断循环,而是把错误信息返回给 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 类。修复策略:
- 所有
interface改class,嵌套对象拆成独立 class - 所有对象字面量改
new Class()+ 逐字段赋值 - 解构改显式索引,spread 改
concat - 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 页面生命周期的一个常见坑:aboutToAppear ≈ onCreate(只执行一次),onPageShow ≈ onResume(每次可见都执行)。
六、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,内容如下:
- 完成工作
- 运动锻炼30分钟
- 读一会书…

记忆功能
你:记住我喜欢用中文回复
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 是完全可行的。通过:
- 砍掉 Docker 依赖:用进程内 ReAct 循环替代容器化执行
- 适配 ArkTS 严格模式:系统性解决 67 个编译错误
- 利用鸿蒙原生能力:SQLite、Preferences、HTTP、FileIO 一应俱全
最终实现了一个能在手机上独立运行的 AI 助手,支持文件操作、网页搜索、记忆、定时任务等完整 Agent 能力。
端侧 AI Agent 是未来的趋势之一——数据不出设备、响应更快、隐私更好。期待更多开发者加入鸿蒙 AI 生态!
项目参考:
- JiuwenClaw:https://openjiuwen.com/jiuwenclaw
- nanoclaw-on-openharmony:https://github.com/lmxxf/openclaw-on-openharmony
- HarmonyOS 开发文档:https://developer.huawei.com/consumer/cn/
开发环境:DevEco Studio + HarmonyOS NEXT 5.0.5 (API 17)
更多推荐



所有评论(0)