从零设计鸿蒙 AI Agent SDK:ArkAgent 架构全解析
一、前言:裸调 LLM API 的五个天花板
假设你要在鸿蒙应用里加一个"AI 助手"。最快的方式是什么?
// 最简版:一个 HTTP 请求搞定
async function ask(question: string): Promise<string> {
const res = await fetch('https://open.bigmodel.cn/api/paas/v4/chat/completions', {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'glm-5.2', messages: [{ role: 'user', content: question }] })
})
const json = await res.json()
return json.choices[0].message.content
}
这段代码能跑通"你问我答"。但产品需求稍微一加码,它就顶不住了:
天花板一:流式输出
用户不想等 5 秒看到一整段文字,他希望像 ChatGPT 一样逐字蹦出来。但裸调 API 是一次性返回 JSON,没有 SSE 解析、没有跨 chunk 的 UTF-8 边界处理、没有增量工具调用拼接。
天花板二:工具调用
用户问"帮我查一下烘焙记录",Agent 需要先调 read_baking_records 工具拿到数据,再基于数据回答。但裸调 API 只发了一次请求、拿了一次响应——响应里说"我要调工具",然后呢?你需要自己实现 Tool Call → Tool Result → 再发请求的循环逻辑。
天花板三:状态持久化
用户聊到一半切了后台,系统把应用杀了。回来后历史消息全没了。你需要自己管消息历史、自己持久化、自己恢复。但什么时候能安全保存?Tool Call 和 Tool Result 配对了吗?保存的时候 API Key 会不会泄漏进去?
天花板四:取消与挂起
用户点了"停止生成",你怎么中断一个已经在途的 HTTPS 流式请求?应用进入后台,你该取消(浪费已消耗的 token)还是挂起(稍后恢复)?挂起后恢复,旧的 HTTP 连接还在吗?
天花板五:安全
API Key 写在哪?rawfile?preferences?日志里?错误信息里?AgentState 持久化时会不会把 Key 序列化进去?HAP 打包后 Key 会不会被提取?
这些问题每一个都很棘手,而且它们互相耦合——你不能单独解决流式输出而不碰工具调用,不能做状态持久化而不碰安全。你需要的是一个整体架构,而不是五个补丁。
ArkAgent 就是为解决这些问题而生的。它的目标不是"帮你发一个 HTTP 请求",而是提供一个生产级的 Agent 运行时,让鸿蒙开发者只需关注业务逻辑(工具定义、系统提示词、UI 交互),不用自己拼装流式解析、状态机、安全边界和生命周期管理。
二、模块边界:为什么分成 core 和 eval?
2.1 三模块架构
ArkAgent 的顶层架构分成三个模块:
┌─────────────────────────────────────────────────┐
│ entry(示例应用) │
│ Demo + 验收 + UI 页面 + 业务工具 │
└────────┬──────────────────────┬──────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ @arkagent/core │ │ @arkagent/eval │
│ Agent 运行时 │◄───│ 评估框架 │
│ │ │ │
│ 对话·工具·流式 │ │ Record/Replay │
│ 状态·技能·记忆 │ │ Grader·Metrics │
│ 子Agent·规划 │ │ Trace·报告 │
└─────────────────┘ └─────────────────┘
依赖规则(铁律):
entry → core ✅ 允许
entry → eval ✅ 允许
eval → core ✅ 允许
core → entry ❌ 禁止
core → eval ❌ 禁止
为什么不让 core 包含 eval?
因为普通应用不应该承担评测、报告和可观测性的依赖。一个聊天应用只需要 @arkagent/core(几十 KB),不需要拉入评估框架(Record/Replay 存储、Grader 注册表、Langfuse 导出器等)。把 eval 独立成单独的 HAR,让"运行 Agent"和"评估 Agent"的依赖彻底分离。
对应的模块选择:
|
你的场景 |
需要哪个包 |
|---|---|
|
给应用加 AI 对话 / 工具 Agent |
只需 |
|
Prompt / 模型升级前做回归测试 |
|
|
离线回放录制好的 LLM 交互 |
|
|
生成评测报告和指标 |
|
2.2 为什么用 HAR 而不是源码依赖?
ArkAgent 以 HAR(HarmonyOS Archive) 包形式分发,而不是要求消费者直接依赖源码目录。
ArkAgent 仓库(SDK 源码)
↓ assembleHar
agent_core.har ← 版本化二进制产物
↓ 复制到业务工程
MyApp/entry/libs/agent_core.har
↓ oh-package.json5 声明
"@arkagent/core": "file:./libs/agent_core.har"
好处:
-
业务工程不会因为 SDK 源码改动而突然失效
-
HAR 是编译过的产物,更接近真实消费者环境
-
可以做版本化管理(记录当前使用的 commit 或版本号)
-
独立消费者验证(
verify-har-consumer.sh在临时项目里真实编译 HAR)
三、core 内部:10 层分层架构
@arkagent/core 内部分成 10 层,每层有明确职责和依赖方向:
agent_core 内部分层
┌──────────────────────────────────────────────────────┐
│ runtime │
│ Agent 状态机、turn/loop 编排、runStream 主路径 │
├──────────────────────────────────────────────────────┤
│ control │ memory │ skill │ planner
│ EventBus │ ContextCompressor │ Skill │ Planner
│ AgentController │ MemoryService │ DirectorySkill │ write_todos
│ Hook Pipeline │ SubAgent │ SkillTools │
├──────────────────────────────────────────────────────┤
│ tool │ state │
│ ToolRegistry │ AgentState │
│ SchemaValidator │ StateStorage SPI │
│ ToolContext │ 原子写入 / 版本迁移 │
├──────────────────────────────────────────────────────┤
│ provider │ transport │
│ ProviderProfile │ HttpTransport SPI │
│ OpenAICompatibleClient│ HarmonyHttpTransport │
│ SseParser │ 取消 / 超时 / 重试 │
│ ToolCallAccumulator │ │
├──────────────────────────────────────────────────────┤
│ domain │ platform │
│ Message / Content │ FileSystem SPI │
│ Model / Tool / Streaming │ HarmonyFileSystem │
│ Cancellation / Error │ HarmonySkillDirectoryAccess │
│ (纯领域值,无平台依赖) │ (HarmonyOS SDK 适配) │
├──────────────────────────────────────────────────────┤
│ core (基础设施) │
│ Json / JsonCodec / Error / Result / Clock / EventBus │
└──────────────────────────────────────────────────────┘
3.1 层职责一览
|
层 |
职责 |
允许依赖 |
|---|---|---|
|
core |
Json、Error、Result、Clock 等基础设施 |
无 |
|
domain |
Message、Model、Tool、Streaming 等纯领域值 |
core |
|
platform |
HarmonyOS 文件、网络、时钟适配 |
HarmonyOS SDK |
|
transport |
HTTP 请求、流式字节、超时、取消 |
platform、domain |
|
provider |
Provider Profile、Wire DTO、Domain 映射 |
domain、transport |
|
tool |
Registry、Schema 校验、Executor、ToolContext |
domain |
|
state |
AgentState、Storage SPI、Schema migration |
domain、platform |
|
control |
EventBus、Controller、Hook 决策 |
domain、runtime contract |
|
skill / memory / planner |
技能、记忆压缩、子 Agent、规划 |
runtime SPI、state、tool |
|
runtime |
Agent 状态机、turn/loop 编排 |
上面所有层 |
3.2 分层原则
依赖必须指向稳定抽象:
✅ runtime → domain(稳定领域值)
✅ provider → transport(稳定 SPI)
❌ domain → provider(领域值不能依赖 Provider 的 Wire DTO)
❌ domain → ArkUI 类型(领域值不能依赖 UI)
❌ domain → HarmonyOS HTTP 类型(领域值不能依赖平台)
这条规则的核心目的是:domain 层必须是纯的。AgentMessage、ModelRequest、ToolCall 这些类型不能出现 ohos.net.http.HttpRequest 或 @kit.ArkUI 的影子。这样 domain 可以在任何环境(包括测试)中实例化,不受平台限制。
3.3 对应源码结构
agent_core/src/main/ets/
├── core/ Result, Json, JsonCodec, Error, Clock, EventBus
├── domain/ Message, Media, Model, Cancellation, Streaming, Tool, ToolRisk
├── codec/ DomainCodec(严格 encode/decode)
├── transport/ HttpTransport, HarmonyHttpTransport, CountingHttpTransport
├── provider/ ProviderProfile, RetryPolicy
├── openai/ SseParser, OpenAIWireCodec, ToolCallAccumulator,
│ IncrementalUtf8Decoder, StreamAssembler, OpenAICompatibleClient
├── providers/ ZhipuProviderProfile, DeepSeekProviderProfile
├── platform/ FileSystem, HarmonyFileSystem, HarmonySkillDirectoryAccess
├── state/ AgentState, AgentStateCodec, StateStorage, FileStateStorage
├── tool/ SchemaValidator, ToolRegistry
├── controller/ AgentController, ControllerEvents
├── hook/ AgentHook, AgentHookPipeline
├── loop/ LoopDetector
├── skill/ Skill, SkillPrompt, SkillTools, DirectorySkill
├── planner/ Planner, PlanMode, PlanStep
├── memory/ ContextCompressor, MemoryService, SubAgent
└── runtime/ AgentRuntime, AgentRuntimeConfig, AgentEvent, AgentRunResult
四、Agent Loop 状态机:整个 SDK 的心脏
Agent Loop 是 ArkAgent 的核心运行逻辑。理解了它,就理解了 Agent 怎么"思考"。
4.1 一轮 Agent Loop 做了什么
用户发一条消息后,Agent 不是简单地"调一次模型返回"。它会进入一个循环:
用户消息
↓
[1] 组装 system prompt + 激活技能 + 规划 + 记忆工具
↓
[2] beforeModelCall Hook(可改写请求 / 直接响应 / 中止)
↓
[3] 调用 LLM(流式输出)
↓
[4] 流式 chunk 经过 onModelChunk Hook
↓
[5] 完整响应经过 afterModelCall Hook(可重试)
↓
[6] 有工具调用吗?
├── 没有 → 进入 Turn Completion → 保存状态 → 结束
└── 有 → 逐个执行 beforeToolCall 决策
↓
[7] 同一批允许的工具并行执行(共享 batch ID)
↓
[8] afterToolCall Hook(可改写 / 停止 / 中止)
↓
[9] 工具结果进入历史,保存状态
↓
回到 [1],进入下一轮
停止条件:
-
模型没有返回工具调用(任务完成)
-
工具返回
stopFlag(主动停止) -
Hook 返回 stop/abort
-
达到最大循环次数
-
循环检测命中
-
用户取消
-
用户挂起
-
未处理的错误
4.2 状态机
用更正式的状态机来表达:
Agent Runtime 状态机
┌──────┐
│ idle │ ←─────────────────────────────────────┐
└──┬───┘ │
│ run / resume │
▼ │
┌────────────┐ ┌──────────────┐ │
│ preparing │────►│ callingModel │ │
└────────────┘ └──────┬───────┘ │
│ │
┌─────────┴──────────┐ │
▼ ▼ │
┌────────────────┐ ┌───────────────────┐ │
│ receivingModel │ │ awaitingToolDecision│ │
└───────┬────────┘ └────────┬──────────┘ │
│ retry │ allowed │
│ (回到 callingModel) ▼ │
│ ┌──────────────┐ │
│ │ executingTools│ │
│ └──────┬───────┘ │
│ │ │
│ ┌───────┴────────┐ │
▼ ▼ ▼ │
┌──────────────┐ ┌──────────┐ │
│ completingTurn│ │ persisting│ │
└──────┬───────┘ └─────┬────┘ │
│ │ │
└───────►┌────────────┐ │
│ persisting │──► completed │
└────────────┘ │
│
┌──────────┐ ┌───────────┐ ┌─────────┐ │
│cancelling│───►│ suspended │───►│ failed │─────┘
└──────────┘ └─────┬─────┘ └─────────┘
│ resume
└──► preparing
4.3 状态不变量
这是几条绝对不能违反的规则:
-
同一 Agent 实例同时只允许一个 active run。不能并行跑两个 runStream。
-
Tool Call 与 Tool Result 必须配对后才形成可恢复检查点。不能在 Tool Call 发出但 Result 未返回时挂起。
-
isRunning=true只表示存在可恢复 run,不表示 HTTP 连接仍存活。挂起后恢复的是 Agent Loop,不恢复旧网络连接。 -
每次状态迁移发布观察事件,控制决策只能通过 Hook。外部不能直接改 Runtime 内部状态。
-
finally 路径必须清理 Transport、Tool 任务和运行标志。即使出错,也不能留泄漏。
五、核心契约:八大设计决策
ArkAgent 的核心契约(Core Contracts)冻结了八个方面的实现决策。每个决策都来自真实的工程踩坑。
5.1 JSON 体系:不用 any
ArkTS 严格模式禁止在公共 API 使用 any。最初尝试的递归类型别名:
// ❌ 被 arkts-no-indexed-signatures 拒绝
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }
编译器直接报错。最终设计的方案是 JsonValue class + JsonKind 枚举 + JsonObject(Map) + 显式访问器。
解码规则:
-
必填字段缺失或类型错误 → 返回
DecodeError -
未知字段 → 默认忽略(Wire DTO 可保留到
extensions) -
未知枚举 → 映射为
unknown并保留 raw value -
解码不抛裸字符串,统一返回
Result<T, ArkAgentError>
5.2 消息模型
enum MessageRole { system, user, assistant, tool }
enum ContentKind { text, image, audio, video, document }
interface AgentMessage {
role: MessageRole
contents: ContentPart[] // 多模态:一段消息可以有多个 Part
toolCallId?: string // tool 角色必须携带
toolCalls?: ToolCall[] // assistant 可携带
reasoningContents?: ContentPart[] // 推理过程(thinking)与正文分开
}
约束:
-
system只允许文本 -
tool必须有toolCallId -
assistant可携带 Tool Call -
文本内容不得用空数组表示
5.3 LLMClient:流式 SPI
interface LLMClient {
generate(request: ModelRequest, signal?: CancellationSignal): Promise<ModelResponse>
stream(request: ModelRequest, observer: StreamObserver<StreamingEvent>,
signal?: CancellationSignal): Promise<StreamSubscription>
}
流式事件固定为 8 种:
|
事件 |
含义 |
|---|---|
|
|
模型调用开始 |
|
|
推理过程增量(thinking) |
|
|
正文增量 |
|
|
工具调用增量 |
|
|
token 用量 |
|
|
重试通知 |
|
|
模型调用完成(必须携带完整 ModelResponse) |
|
|
错误 |
关键设计:modelComplete 必须携带完整的 ModelResponse,onComplete 回调不替代这个事件。这样消费者可以在 modelComplete 里一次性拿到完整响应,同时通过 delta 事件实现了流式。
5.4 工具契约
interface ToolDefinition {
name: string
description: string
parameters: JsonObject // JSON Schema
}
interface ToolContext {
sessionId: string
state: ReadonlyAgentState // 只读,工具不能改 Agent 状态
batchCallId: string // 同一批工具共享
signal: CancellationSignal // 工具可响应取消
services: ToolServiceRegistry
}
interface ToolExecutor {
execute(arguments: JsonObject, context: ToolContext): Promise<ToolResult>
}
关键设计决策:
-
工具用对象参数,不用反射式调用。ArkTS 不支持
Function.apply和动态分发,所以我们选择了显式的execute(arguments, context)签名。 -
ToolContext 显式传入,不提供全局 current context。这保证了工具执行的隔离性和可测试性。
-
参数必须在执行前完成 JSON 解析和最小 Schema 校验。不信任模型生成的参数。
-
工具名称在 Registry 内唯一。
5.5 错误模型:分层 + 可重试性
enum ErrorLayer { decode, config, transport, provider, tool, runtime, storage, security, cancelled }
enum ErrorRetryability { never, safe, conditional }
interface ArkAgentError {
layer: ErrorLayer
code: string
message: string
retryability: ErrorRetryability
statusCode?: number
providerCode?: string
}
重试规则:
|
场景 |
分类 |
自动重试 |
|---|---|---|
|
401/403 |
provider auth |
永不 |
|
429 |
rate limit |
尊重 Retry-After,条件重试 |
|
500/502/503/504 |
provider unavailable |
指数退避,有限次数 |
|
用户取消 |
cancelled |
永不 |
|
Tool 抛错 |
tool |
转换为 ToolResult error |
关键区分:用户取消与超时不同;暂停是受控 Runtime 结果,不是 Provider 错误。
5.6 Agent 事件
使用单一 envelope:
interface AgentEvent {
eventId: string
runId: string
sessionId: string
sequence: number // 单次 run 内严格递增
timestamp: number
type: AgentEventType
payload: AgentEventPayload
}
5.7 AgentState
AgentState 固定包含:
schemaVersion、sessionId
history(消息 + 情节记忆)
usages、metadata
plan、activeSkills
isRunning、totalLoopCount、currentLoopCount、currentLoopUsages
lastError
systemReminders、systemPromptHistory、toolsHistory
pendingRunCheckpoint
铁律:状态不得包含 Client、Tool Executor、Hook、监听器、CancellationSignal 或 API Key。
5.8 版本策略
-
State 从
schemaVersion = 1开始 -
minor 版本只追加可选字段或新枚举 raw fallback
-
删除/改义字段必须升级 schemaVersion 并提供迁移器
-
公共契约在 0.x 可通过 ADR 变更,1.0 后遵循语义化版本
六、主数据流:从用户输入到最终回答
把前面的架构串起来,看一条完整的数据流:
用户输入 "帮我查烘焙记录并算失重率"
│
▼
[1] 组装请求
system prompt + 激活技能 + 用户消息 → Domain ModelRequest
│
▼
[2] beforeModelCall Hook
可改写请求 / 直接响应 / 中止
│
▼
[3] Provider Adapter
Domain ModelRequest → Wire DTO(OpenAI JSON 格式)
Zhipu/DeepSeek Profile 做 requestTransform
│
▼
[4] HarmonyOS Transport
发送 HTTPS POST,接收 SSE 流
│
▼
[5] SSE / JSON Decoder
data: chunk → SseParser → IncrementalUtf8Decoder
→ StreamingEvent(reasoningDelta / textDelta / toolCallDelta)
│
▼
[6] Accumulator
textDelta → 拼接完整文本
toolCallDelta → ToolCallAccumulator 拼接完整工具调用
│
▼
[7] afterModelCall Hook
可重试
│
▼
[8] 有工具调用?
│
├── 没有 → Turn Completion → 保存状态 → 返回最终文本
│
└── 有 read_baking_records + calculate_weight_loss
│
▼
[9] beforeToolCall Hook + 风险分级
safe → 直接执行
danger → 进入 PendingApproval,等待用户确认
│
▼
[10] Tool Executor + ToolContext
read_baking_records → 返回三条记录
calculate_weight_loss → 返回失重率
│
▼
[11] afterToolCall Hook
│
▼
[12] ToolResult → 加入消息历史
│
▼
[13] StateStorage checkpoint(原子保存)
│
▼
回到 [1],模型基于工具结果继续回答
模型输出最终文本 → Turn Completion → 结束
整个过程对 UI 层只暴露两种东西:AgentEvent(流式事件)和 AgentRunResult(最终结果)。
七、Provider 兼容:智谱与 DeepSeek
ArkAgent 1.0 的一级 Provider 是智谱 GLM和DeepSeek,两者都兼容 OpenAI Chat Completions 接口。
7.1 共享 Profile
两家均使用 Bearer Token + /chat/completions,共享:
-
请求消息、tools、tool_choice、stream 的基础 Wire DTO
-
SSE
data:解帧与[DONE] -
choice/delta/content/tool_calls/usage 映射
-
ToolCallAccumulator
-
HTTP 状态和 Provider error envelope 处理
7.2 Provider 差异
每个 Provider 有自己的 Profile,声明差异化的能力:
|
差异点 |
智谱 GLM |
DeepSeek |
|---|---|---|
|
端点 |
|
|
|
推理过程 |
|
|
|
流式工具 |
可能需 |
标准 OpenAI 格式 |
|
finish reason 额外值 |
|
|
|
图片输入 |
支持( |
不支持(1.0 范围外) |
关键设计:
-
模型名不进入 Core enum。Profile 只提供建议默认值和能力探测信息。
-
Provider 特有参数只进入
ModelConfig.extensions,由 Profile 消费。 -
未消费的扩展必须在调用前报配置错误,不能静默发送。
7.3 Provider Profile 的职责
每个 Provider Profile 必须声明:
providerId 唯一标识
baseUrl API 地址
chatPath 聊天路径
defaultHeaders 默认请求头
capabilities 能力声明(图片/文档/流式工具等)
requestTransformer 请求转换(Domain → Wire)
responseTransformer 响应转换(Wire → Domain)
streamTransformer 流式转换
errorTransformer 错误转换
八、踩坑与设计教训
8.1 ⚠️ 踩坑一:ArkTS 禁止递归 JSON 类型
症状:最初设计的 JSON 类型用了递归别名:
type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject
interface JsonObject { [key: string]: JsonValue }
编译器报 arkts-no-indexed-signatures,直接拒绝。
根因:ArkTS 严格模式禁止字符串索引签名([key: string]: T),因为它破坏了类型安全。递归别名 + 索引签名的组合无法编译。
修复:设计 JsonValue class + JsonKind 枚举 + JsonObject(Map<string, JsonValue>),用显式访问器代替索引签名。JSON 文本由不依赖 JSON.parse 动态结果的严格 Parser 处理。
教训:在 ArkTS 里处理 JSON,不能照搬 TypeScript 的 any 或递归类型。必须设计一套类型安全的 JSON 体系,虽然写起来更啰嗦,但换来了编译期保证。
8.2 ⚠️ 踩坑二:HTTP 流可能被缓冲
症状:在某些网络环境下,HTTPS 流式响应被中间层缓冲,导致 onNext 回调不及时,用户看不到逐字输出。
根因:HarmonyOS HTTP 栈在某些场景下可能缓冲响应体,不保证逐 chunk 回调。这是平台行为,不是代码 bug。
修复:Transport 层设计了 SPI(HttpTransport),不绑死 HarmonyOS HTTP 实现。如果默认 HarmonyHttpTransport 在某些设备上有缓冲问题,可以替换为原生扩展或替代库。这个问题在真机 spike 中持续观察。
教训:流式输出是 Agent 体验的核心,但它依赖平台 HTTP 栈的行为。必须把 Transport 设计为 SPI,留出替换空间。
8.3 ⚠️ 踩坑三:密钥可能泄漏到持久化状态
症状:如果不做显式隔离,API Key 可能在 AgentState 序列化时被写入文件,或者在错误日志中被打印。
根因:密钥管理是一个横切关注点——它涉及 Provider 配置、状态持久化、错误日志、调试输出等多个层面。如果不从架构层面规定"密钥不能出现在哪些地方",很容易在某条路径上泄漏。
修复:从契约层面规定铁律:
AgentState 不得包含:
✗ Client 实例
✗ Tool Executor
✗ Hook
✗ 监听器
✗ CancellationSignal
✗ API Key / Authorization Header
密钥通过 BearerTokenProvider 接口传入,ProviderConfig 接收 secret provider,State 和日志永不持久化 secret。
RuntimeCredentials 只在内存中持有 Key:
export class RuntimeCredentials {
static shared(): RuntimeCredentials { ... }
setApiKey(provider: string, key: string): void { ... } // 内存
getApiKey(provider: string): string { ... }
clearAll(): void { ... } // 页面/Ability 销毁时调用
}
教训:密钥安全不能靠"小心一点",必须从架构契约层面禁止。
8.4 ⚠️ 踩坑四:取消不是"关掉 HTTP 请求"那么简单
症状:用户点"停止",HTTP 请求中断了,但 Tool 正在执行副作用(比如写文件),被强行打断后留下脏数据。
根因:取消不只是网络层面的中断,它涉及整个 Agent Loop 的多个阶段——模型调用、工具执行、状态持久化。每个阶段对取消的响应不同。
修复:设计了分层的取消模型:
-
CancellationSignal贯穿整个 runStream 生命周期 -
Transport 层:取消中断 HTTP 连接
-
Tool 层:Tool 通过
context.signal检查取消状态,自己决定如何安全终止 -
Runtime 层:取消后进入
cancelling状态,finally 路径清理所有资源
关键决策:取消后不可恢复——取消意味着"这次运行作废"。如果要"稍后继续",用挂起(suspend),它只在安全 checkpoint 挂起,恢复时创建新的网络调用。
九、能力地图:ArkAgent 1.0 覆盖什么
ArkAgent 1.0 能力覆盖
├── 对话
│ ├── 单轮 / 多轮对话
│ ├── 流式输出(SSE + 增量 UTF-8)
│ ├── 推理过程(reasoning_content / thinking)
│ └── 多模态输入(图片 / 文档)
│
├── 工具调用
│ ├── ToolDefinition + JSON Schema 参数
│ ├── ToolRegistry 注册与校验
│ ├── 并行执行(同 batch)
│ ├── 风险分级 + 人工审批
│ └── 循环检测(LoopDetector)
│
├── 状态与生命周期
│ ├── AgentState 持久化
│ ├── 原子写入(tmp + replace)
│ ├── schemaVersion 版本迁移
│ ├── 挂起 / 恢复(安全 checkpoint)
│ └── Ability 生命周期绑定
│
├── 控制
│ ├── AgentController + EventBus
│ ├── Hook 体系(beforeModelCall / afterModelCall / beforeToolCall / ...)
│ └── 计划审批(PlanMode.review)
│
├── 技能
│ ├── 对象技能(always-on / 按需激活)
│ ├── 目录技能(SKILL.md 自动发现)
│ └── write_todos 规划器
│
├── 记忆
│ ├── 上下文压缩(ContextCompressor)
│ ├── EpisodicMemory + retrieve_memory
│ └── 子 Agent(delegate_task / clone)
│
├── Provider
│ ├── 智谱 GLM(glm-5.2 / glm-4.6v)
│ ├── DeepSeek(deepseek-v4-flash)
│ └── OpenAI 兼容 Profile 扩展点
│
└── 安全
├── BearerTokenProvider 密钥隔离
├── HTTPS only + 禁止重定向
├── 密钥不入 State / 日志 / 文件
└── scan-hap-secrets.sh 发布扫描
1.0 明确不做的:
-
JavaScript Runtime(鸿蒙无 Node、安全边界不可控)
-
OpenAI Responses / Gemini / Claude / Bedrock
-
音频 / 视频输入
-
应用内主题切换(那是 UI 框架的职责)
十、最佳实践清单
10.1 架构设计
-
✅ 把"运行 Agent"和"评估 Agent"分成两个包,普通应用不该承担评估依赖。
-
✅ Core 内部按 10 层分离,依赖必须指向稳定抽象。
-
✅ domain 层必须是纯的——不能出现 ArkUI 类型或 HarmonyOS HTTP 类型。
-
✅ Transport 层设计为 SPI,不绑死 HarmonyOS HTTP 实现。
-
✅ Provider 差异通过 Profile 隔离,模型名不进入 Core enum。
10.2 Agent Loop
-
✅ 同一 Agent 实例同时只允许一个 active run。
-
✅ Tool Call 与 Tool Result 必须配对后才形成可恢复检查点。
-
✅ 取消不可恢复;挂起可恢复,但只在安全 checkpoint。
-
✅ 恢复的是 Agent Loop,不恢复旧网络连接。
-
✅ finally 路程必须清理 Transport、Tool 任务和运行标志。
10.3 安全
-
✅ 密钥通过
BearerTokenProvider传入,不直接出现在ProviderConfig字符串字段。 -
✅
AgentState永不包含 API Key、Authorization Header 或 Client 实例。 -
✅ 日志不得包含 Authorization、原始 API Key 或未脱敏请求体。
-
✅ 页面/Ability 销毁时清除内存凭据并丢弃持有 Token 的 Runtime。
-
✅ 发布 HAP 前执行密钥扫描。
10.4 业务工程
-
✅ UI 不直接创建 Provider 和 Runtime,通过 Service/Coordinator 层管理。
-
✅ 每个会话有稳定且互不冲突的
sessionId。 -
✅ Provider 切换时销毁旧 Runtime。
-
✅ App 进入后台时根据业务选择取消或
requestSuspend。 -
✅ Tool 副作用、危险操作和用户确认留在宿主 App 中实现。
十一、常见错误对照表
|
错误做法 |
问题 |
正确做法 |
|---|---|---|
|
裸调 HTTP API 做 Agent |
无流式、无工具循环、无状态管理 |
用 AgentRuntime |
|
把 API Key 写入 rawfile / preferences |
HAP 可被提取 |
运行时内存输入,页面销毁清除 |
|
AgentState 里存 Client/Key 实例 |
持久化时泄漏密钥 |
State 只存可序列化非敏感数据 |
|
用 |
ArkTS 编译拒绝 |
用 JsonValue class + JsonKind |
|
并行跑两个 runStream |
状态冲突、资源竞争 |
同一实例同时只允许一个 run |
|
取消后尝试恢复旧连接 |
连接已断 |
取消不可恢复;用 suspend |
|
模型名硬编码在 Core enum 里 |
模型更新需改 SDK |
模型名作为配置传入 |
|
Tool Call 未配对就挂起 |
恢复后状态不一致 |
Call/Result 配对后才 checkpoint |
|
domain 层依赖 ArkUI / HTTP 类型 |
破坏分层纯净性 |
domain 只依赖 core |
|
把 eval 依赖混入 core |
普通应用承担不必要的依赖 |
eval 独立 HAR,单向依赖 core |
|
静默发送未识别的 Provider 扩展 |
Provider 拒绝或行为异常 |
未消费扩展必须报配置错误 |
|
错误信息拼接请求 Header |
泄漏 Authorization |
只显示错误码和必要说明 |
十二、验证清单(真机 Review)
基础对话
-
运行时输入 API Key,能完成普通文本生成
-
流式输出逐字显示,不卡顿
-
连续多轮消息符合预期
-
推理过程(reasoning)与正文分开显示
工具调用
-
Agent 先调工具,再基于结果回答
-
多个工具在同轮内并行执行
-
工具参数通过 Schema 校验
-
危险工具触发人工审批流程
状态与生命周期
-
切后台后回来,历史消息不丢失
-
挂起后恢复能继续之前的任务
-
取消后网络与 Runtime 均停止
-
重启 App 后能恢复上次会话
安全
-
错误页面不显示 Key / Authorization / 完整请求体
-
卸载/重装后 Key 不会自动恢复
-
scan-hap-secrets.sh扫描无密钥泄漏 -
AgentState 持久化文件中无 API Key
十三、构建验证
# 构建 HAP(示例应用)
NODE_HOME=/Applications/DevEco-Studio.app/Contents/tools/node \
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHap --no-daemon
# 构建 HAR(SDK 产物)
NODE_HOME=/Applications/DevEco-Studio.app/Contents/tools/node \
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHar --no-daemon
# 运行测试
NODE_HOME=/Applications/DevEco-Studio.app/Contents/tools/node \
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw test --no-daemon
ArkAgent 1.0 构建结果:
CompileArkTS passed
PackageHap passed
BUILD SUCCESSFUL
十四、写在最后
在鸿蒙上做 AI Agent,看似只是"调一个 API",背后却涉及流式传输、工具循环、状态持久化、取消/挂起/恢复、密钥安全、多 Provider 兼容、子 Agent 委派等多个维度。如果这些问题留给每个业务页面自己解决,代码会迅速退化为不可维护的意大利面。
ArkAgent 的架构哲学是:
把复杂度集中在 SDK 层,让业务代码只关心"做什么",不关心"怎么跑"。
记住这几条口诀:
运行评估要分包,普通应用不背评估债。 十层分层依赖稳,domain 层必须是纯的。 Agent Loop 是心脏,Think-Act-Observe 转圈圈。 Tool Call Result 要配对,配对之后才 checkpoint。 取消不可逆,挂起可恢复但不开旧连接。 密钥只走 Provider,State 日志永不存。 ArkTS 禁 any,JsonValue 是正路。 Transport 做成 SPI,流被缓冲能替换。
本文是 ArkAgent 鸿蒙教程系列的第一篇,从架构全景出发。后续文章会逐层深入——流式输出、工具调用、状态管理、记忆子 Agent、评估框架——每一层都有完整的实战代码和踩坑记录。
如果你正在鸿蒙上做 AI 应用,可以直接复用 ArkAgent 的架构分层和设计决策作为起点。不要从"一个 HTTP 请求"开始——那是一条走不远的路。
更多推荐



所有评论(0)