一、前言:裸调 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

只需 @arkagent/core

Prompt / 模型升级前做回归测试

@arkagent/core + @arkagent/eval

离线回放录制好的 LLM 交互

@arkagent/core + @arkagent/eval

生成评测报告和指标

@arkagent/core + @arkagent/eval

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 层必须是纯的AgentMessageModelRequestToolCall 这些类型不能出现 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 状态不变量

这是几条绝对不能违反的规则

  1. 同一 Agent 实例同时只允许一个 active run。不能并行跑两个 runStream。

  2. Tool Call 与 Tool Result 必须配对后才形成可恢复检查点。不能在 Tool Call 发出但 Result 未返回时挂起。

  3. isRunning=true 只表示存在可恢复 run,不表示 HTTP 连接仍存活。挂起后恢复的是 Agent Loop,不恢复旧网络连接。

  4. 每次状态迁移发布观察事件,控制决策只能通过 Hook。外部不能直接改 Runtime 内部状态。

  5. 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 种:

事件

含义

modelStart

模型调用开始

reasoningDelta

推理过程增量(thinking)

textDelta

正文增量

toolCallDelta

工具调用增量

usage

token 用量

retry

重试通知

modelComplete

模型调用完成(必须携带完整 ModelResponse)

error

错误

关键设计:modelComplete 必须携带完整的 ModelResponseonComplete 回调不替代这个事件。这样消费者可以在 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 是智谱 GLMDeepSeek,两者都兼容 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

端点

open.bigmodel.cn/api/paas/v4/

api.deepseek.com

推理过程

{thinking:{type:"enabled"}} 扩展体

thinking.type + reasoning_effort

流式工具

可能需 tool_stream=true

标准 OpenAI 格式

finish reason 额外值

sensitivenetwork_error

insufficient_system_resource

图片输入

支持(glm-4.6v 等视觉模型)

不支持(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 只存可序列化非敏感数据

any 处理 JSON 响应

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 请求"开始——那是一条走不远的路。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐