鸿蒙应用接入 AI 智能体实战:别再只做聊天框了,真正可落地的“应用 + 智能体”方案来了
做鸿蒙应用开发这两年,我最大的一个感受是:AI 能力接入这件事,大家普遍卡在“聊天框”这一层就不动了。模型能回话、能推荐、能扯闲篇,但一旦用户说出“帮我把这笔订单退款”“帮我创建一个日程并提醒我”这种具体诉求,应用就哑火了。不是大模型不行,而是应用压根没把“对话”和“业务动作”连起来。
我前阵子做一个鸿蒙端的直播互动工具时,被“观众发来一条投诉消息,智能体只会回‘我理解您的心情’,但没法真正查订单、建工单”这种半吊子体验折磨得够呛。后来把方案彻底改成了“应用 + 智能体”的协作结构,事情才真正变得可落地。这篇文章把我踩过的坑、验证过的方案、以及一套可以直接搬走的接入思路,完整分享出来。尤其适合正在鸿蒙应用里接 AI、又不想只做一个花架子聊天页面的开发者。
1. 能回答问题的聊天框和能替你干活的智能体,差在哪
先说清楚一个基础认知:聊天框和智能体,本质上是两种技术架构。聊天框解决的是“信息生成”,智能体解决的是“任务完成”。
1.1 只做聊天框,用户得到的是一句安慰而不是一个结果
我们最早在鸿蒙应用里接大模型时,走的是一条最简单的路:用户输入文本,我们组装 Prompt,请求模型接口,拿到回答后流式展示在页面上。这套链路跑通非常快,但用户的实际使用数据很难看——聊天的开口率挺高,真正把对话推进到“产生业务价值”比例却低得可怜。
举一个真实场景:一个用户在直播互动页面里发了一句“我要投诉,我的订单一直没收到货,你们到底管不管”。
聊天框方案下,模型会输出一段相当得体的安抚话术,比如“非常抱歉给您带来不好的体验,我这边帮您反馈给相关同事,请您耐心等待”。用户看到这句话之后,通常只会做两件事:要么继续追问“那到底什么时候能解决”,要么退出页面去联系人工客服。
问题的根子在于:模型没有权限、也没有能力去查询订单状态、生成投诉工单、触达人工客服系统。它所有的“反馈给相关同事”都是话术层面上的承诺,背后没有任何动作发生。用户在等,业务在睡,最后的结果就是用户觉得 AI 是假的,是套壳的。
1.2 智能体的核心是“脑子 + 手脚”:模型负责想,应用负责做
真正可落地的智能体,在我看来必须满足三个特征:
- 有意图识别能力:能判断用户说这段话,是想闲聊、想咨询,还是想触发一个具体业务动作。
- 有工具调用能力:能根据当前任务,主动调用应用提供的接口、函数、数据服务,而不是只靠模型内部知识硬答。
- 有状态管理能力:能在一个多轮对话里记住目标、保存中间结果,并在后续轮次中继续推进未完成的任务。
用一句话概括:模型是大脑,应用注册的一个个工具函数是手脚。大脑负责判断现在要做什么,手脚负责把事办成。
在这个框架下,刚才那条投诉消息的处理路径就变成了:
- 模型识别出用户意图是“投诉/售后”。
- 模型向应用发起一个工具调用请求,比如
query_order(orderId=xxx)。 - 应用执行查询,把订单状态、物流信息返回给模型。
- 模型根据结果生成一句有实际依据的回复,并继续调用
create_work_order(orderId=xxx, reason=xxx)。 - 工单创建成功,用户看到的是“您的订单确实还在运输中,我已为您加急联系物流,并生成售后工单,编号 AB123”。
- 应用侧弹出一个真实的工单进度查询入口。
这才是“应用 + 智能体”该有的样子:AI 在前面做判断和沟通,应用在后面做执行和反馈。用户感知到的不再是AI聊天,而是AI办事。
1.3 MCP 这类工具协议,到底在智能体架构里扮演什么角色
项目里很多同学会问:智能体的工具调用,是不是一定要上 MCP(Model Context Protocol)这类标准化协议?
我个人理解是:协议是手段,不是目的。MCP 的价值在于把“工具的定义、发现、调用”标准化,让同一个模型可以对接不同系统里的不同工具,降低集成成本。如果你的应用只服务自己一个智能体,自定义一套工具调用 JSON Schema 也完全没问题。但如果你的应用打算对外开放能力,或者接入多个不同的 Agent 平台(比如 Coze 智能体、Dify 智能体),那用标准化协议会更省事,避免每个平台适配一套接口。
后面我会给出一套不依赖 MCP、直接用普通 HTTP 接口实现工具调用的方案。看懂这套逻辑,再去看 MCP 的文档,会发现本质都一样:模型输出结构化指令 -> 应用执行 -> 结果回传 -> 模型继续生成。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 鸿蒙侧真正需要的不是 SDK,而是一个“任务编排层”
最开始我以为鸿蒙接智能体的难点在于怎么调模型 API,后来发现 API 调用反而是最简单的部分。真正的核心难点在于:模型输出的是一段文字或结构化指令,应用怎么把它变成真实的业务动作,并且整个过程对用户来说平滑无感。
2.1 方案选型:两条路线的对比
在我调研和实测下来,鸿蒙应用接入智能体主要有两条路线:
| 路线 | 做法 | 适合场景 | 缺点 |
|---|---|---|---|
| 路线 A:直接对接模型 API + 自定义工具路由 | 应用直接请求大模型接口,在应用层实现意图识别、工具调用解析、业务函数注册 | 产品形态固定、业务流程明确、团队对代码掌握度高 | 需要自己处理 Prompt、工具定义、状态管理,开发量较大 |
| 路线 B:接入智能体平台(Coze / Dify 等) | 在平台上编排 Agent 工作流,应用通过 SDK 或 API 透传用户消息,接收平台返回的结果和工具调用事件 | 业务方想快速上线、非研发人员也要能调整智能体逻辑 | 依赖第三方平台,定制能力和私有化部署受限,鸿蒙端 SDK 支持程度要额外评估 |
从我在鸿蒙上的实践来看,多数团队一开始适合走路线 B 快速验证,但最终如果想把智能体能力深度绑进应用业务,最后都要回到路线 A——因为鸿蒙应用里很多工具调用是本地能力,比如拉起系统分享、创建本地日历日程、读取设备状态、跳转应用内页面,这些操作在智能体平台的沙箱环境里很难直接编排。
我最终在直播互动工具里采用的是混合方案:模型对话和通用推理放在第三方智能体平台,业务工具调用走本地函数注册。用户在聊天过程中提到“投诉”“退款”“查订单”,平台返回的不是纯文本,而是一个带 tool_use 标记的结构化指令;鸿蒙端收到后执行本地代码,再回传结果给模型继续生成用户可见的回复。
2.2 鸿蒙应用里,智能体“工具函数”的设计思路
工具函数是智能体手脚的具体体现。设计时我给自己定了几条原则:
- 一个工具只做一件事。别让模型猜“这个函数到底能查订单还是能退款”,函数名和描述足够清晰。
- 入参尽量少且类型明确。模型从用户话术里抽取参数本来就容易出错,入参越复杂,抽取成功率越低。
- 返回值要结构化。返回纯文本会让模型二次理解,直接返回 JSON 最稳妥。
- 所有工具都要有兜底错误处理。模型调用工具传入的参数,极有可能不符合应用接口预期,工具层必须对非法参数返回明确错误信息,而不是直接崩溃。
举个例子,查询订单工具的定义看起来是这样的:
json复制{
"name": "query_order",
"description": "根据订单号查询订单详情,包括订单状态、商品信息、物流信息",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "用户的订单号"
}
},
"required": ["order_id"]
}
}
模型输出的一条工具调用指令是这样的:
json复制{
"tool_use": {
"tool": "query_order",
"arguments": {
"order_id": "HD20240618001"
}
}
}
鸿蒙端拿到这段 JSON,在本地路由表里找到对应函数,执行,然后把结果塞回给模型。这一整套链路,本质上就是我在应用里用 ArkTS 手写的一个轻量级任务编排层。不需要什么高深的框架,只要你把路由表、参数校验、结果回传这三件事做扎实,智能体就能真正干起活来。
2.3 为什么不要把所有逻辑都塞进 Prompt
很多团队为了省事,把工具描述和业务规则全部写进系统 Prompt,希望模型“顺便”就把事办了。这种做法在功能演示里没问题,一旦上生产,你会遇到几个很难受的问题:
- Prompt 过长导致响应时间和成本上升。每次请求都在重复传输一大段工具定义,浪费 token。
- 模型遵循度不稳定。同一个指令,今天可能调用工具,明天可能直接在话术里编造一个结果,没有任何调用动作。
- 业务规则更新不及时。你改了工具定义,模型侧的旧缓存可能还按老规矩生成调用,导致返回的参数字段对不上。
所以在鸿蒙实践里,我倾向于把“模型理解业务”这件事放到最小闭环:模型只需要知道“有哪些工具、什么情况下用、参数是什么”,而真正的业务规则判断放到应用层。比如“什么样的订单可以退款”“退款金额怎么计算”,这些规则不应交给模型自由发挥,应该由本地函数执行时校验。
3. 手把手接入:从一条用户消息到一次真实业务动作的完整链路
下面我用鸿蒙应用里一个“直播互动 + 售前咨询”的案例,把完整链路拆开。场景是这样的:用户在看直播时,通过直播间的消息入口发消息,智能体需要判断消息意图,如果是咨询商品就回答,如果是售后诉求就调用订单工具并生成工单。
3.1 用户消息进入智能体的前置准备
接入之前,先把三样东西准备好:
- 模型服务的访问凭证:无论你用哪个平台,都需要一对 API Key 或 Access Token,在鸿蒙应用里务必放到服务端转发,不要直接内置在客户端里,否则打包出去就等于公开了你的账单。
- 会话标识:给每个用户生成一个
session_id,后续多轮对话都带这个 ID 上报,平台端负责维护上下文。 - 工具协议定义:把允许智能体调用的工具清单,按平台要求的格式注册好。
我走的路线 A,模型侧用的是一个兼容 OpenAI 协议的服务,鸿蒙端通过 HTTP 请求对话接口。请求体大致长这样:
json复制{
"model": "agent-chat",
"messages": [
{
"role": "user",
"content": "我要投诉,订单 HD20240618001 一直没收到货"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "query_order",
"description": "查询订单详情",
"parameters": {
"type": "object",
"properties": {
"order_id": { "type": "string" }
},
"required": ["order_id"]
}
}
}
]
}
鸿蒙端用 ArkTS 发起请求时,我用的是 @ohos.net.http 模块,把上面这个 JSON 序列化后 POST 到模型网关。这里有一个细节:实时性要求高的场景,不要用普通的短连接轮询,最好走 SSE 流式接口,让模型输出逐字上屏,用户感知会好很多。
3.2 收到模型返回的 tool_use 指令后,本地怎么执行
模型在收到上面那条投诉消息后,可能返回两种结果。第一种是它觉得不需要调用工具,直接生成回复文本;第二种是它判断需要调用 query_order,返回值里带上了工具调用指令。
关键处理逻辑在第二种。我写了一个统一的响应解析函数,核心代码如下:
typescript复制function handleAgentResponse(rawResp: string): AgentActionResult {
const parsed = JSON.parse(rawResp) as AgentResponse;
// 优先判断是否有工具调用指令
if (parsed.tool_calls && parsed.tool_calls.length > 0) {
const toolCall = parsed.tool_calls[0];
const toolName = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
const toolResult = executeLocalTool(toolName, args);
// 把执行结果作为一条“工具结果消息”回传给模型
return {
needContinue: true,
toolCallId: toolCall.id,
toolResult: toolResult,
};
}
// 没有工具调用,直接展示模型回复
return {
needContinue: false,
replyText: parsed.content,
};
}
executeLocalTool 是一个本地路由函数,内部用一个 Map 维护工具名和实际函数的对应关系:
typescript复制const toolRegistry = new Map<string, (args: any) => Object>();
toolRegistry.set("query_order", (args) => {
// 真实业务:查询本地订单数据库或调用服务端接口
const order = orderService.queryByOrderId(args.order_id);
if (!order) {
return { success: false, error: "订单不存在" };
}
return {
success: true,
data: {
order_id: order.order_id,
status: order.status,
logistics: order.logistics,
},
};
});
执行完工具后,应用要把结果再发回模型,让模型基于真实数据生成用户可读的回复。这时请求消息列表里会多一条角色为 tool 的消息,内容就是工具返回的 JSON 结果。模型看到这份结果后,生成的回复就不再是空泛的安抚,而是有具体订单状态的说明。
3.3 一个完整闭环的代码串联
把上述环节串联起来,鸿蒙端的核心流程是一个 async 方法:
typescript复制async function handleUserMessage(sessionId: string, userText: string): Promise<string> {
let messages = buildInitialMessages(userText);
let finalReply = "";
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
const resp = await requestAgentChat(sessionId, messages);
const result = handleAgentResponse(resp);
if (!result.needContinue) {
finalReply = result.replyText;
break;
}
// 追加工具调用记录和工具结果到消息列表
messages.push({
role: "assistant",
tool_calls: [{
id: result.toolCallId,
function: {
name: result.toolName,
arguments: JSON.stringify(result.toolArgs),
},
}],
});
messages.push({
role: "tool",
tool_call_id: result.toolCallId,
content: JSON.stringify(result.toolResult),
});
}
return finalReply;
}
MAX_TOOL_ROUNDS 一定要设置,我一般设为 3。防止模型陷入“反复调用工具不返回最终回复”的死循环。另一种极端情况是模型调了一个工具后又调另一个,虽然也可以通过循环支持,但每多一轮,用户等待时间都在增加,所以要给整个流程一个总超时时间,超时后直接返回“处理中,请稍后查看结果”,不能让用户永远转菊花。
3.4 流式输出下的工具调用处理有点不一样
如果页面追求丝滑体验,你肯定想用流式输出,让模型一个字一个字打出来。但流式模式下工具调用的处理逻辑会有变化:模型可能先吐出一段文本,紧接着输出一个工具调用标记,然后又继续吐文本。
我实测中的处理方案是:流式推送过来的内容,不要立刻上屏,先做缓冲判断。如果发现当前片段是文本,直接追加到文本展示区;如果发现片段里带 tool_calls 标记,就停止上屏,把工具调用部分交给本地执行,执行完成后把结果以一条不可见的内部消息发回给模型,继续消费后续流式输出。这样用户看到的体验是:AI 先说了判断,然后停顿了一下(工具执行期间),接着把最终结果给出来。
这个停顿最好不要超过 1.5 秒,否则用户会以为 AI 卡死了。工具执行如果耗时较长,我会在 UI 上给一个轻量的“正在查询”状态提示,和打字光标区分开。
4. 把“能跑通”变成“能上线”:状态恢复、任务分发与权限闭环
Demo 能跑通和能上线之间,隔着一大堆生产环境问题。这些问题不处理,用户量一上来,智能体就会变成事故现场。
4.1 应用被杀、进程回收后,进行中的智能体任务怎么办
鸿蒙应用在后台被系统回收是非常常见的事。用户和智能体聊到一半,切到别的应用,再回来,如果发现“刚才说到哪了”完全丢了,这个产品基本就废了。
我的做法是:每个会话阶段都持久化。会话 ID、消息列表、当前语义上下文,在每轮请求发起之前同步写入本地数据库(我用的 @ohos.data.relationalStore)。用户重新打开应用时,先尝试恢复本地会话,再决定是否继续向模型发起请求。如果模型平台侧上下文已过期,至少保留用户可见的历史消息,不让对话“断片”。
这里有一个更小的细节:不要把模型的中间思考过程全部塞进持久化里。你只需要保存两类内容——用户消息和应用侧的业务动作结果(比如“已创建工单 AB123”),模型那些冗长的内部推理过程没必要存,存了还占空间。
4.2 异步任务:智能体不是每次都能立刻给出结果
不是所有工具都能在几十毫秒内返回结果。比如“创建退款申请”可能需要后端审核、“生成周报”可能需要等待另一个系统回调。这时的处理逻辑就不能是同步等待,而要转换成异步任务。
我在鸿蒙端做了一个简单的任务轮询表:应用把异步任务 ID 写入本地存储,同时注册一个后台任务检查器,定时向服务端询问任务状态。当服务端回传“任务已完成”时,应用通过本地通知栏给用户推送一条结果消息,用户点开通知直接进入对应结果页。
这种“异步任务 + 主动通知”的方式,才是智能体在真实应用里该有的形态。聊天框只能做到“你问我答”,而智能体应该做到“你委托我办事,办完我通知你”。
4.3 权限与安全:不要让智能体拥有“万能执行权”
智能体接入时最容易被忽视的,是工具调用权限边界。如果一个智能体能查订单、能退款、能改密码、能发消息,那一旦 Prompt 被恶意注入,或者模型被诱导发起非预期调用,后果非常严重。
我定的权限控制原则:
- 所有工具函数按最小权限划分,不做“一个大函数干所有事”的设计。
- 涉及敏感操作的函数,必须额外校验用户的身份凭证,不能只凭模型传参就执行。
- 应用在调用本地工具前,检查当前用户是否登录、订单是否属于该用户、操作是否在业务允许的时间窗口内。
- 对模型返回的工具参数做严格校验,非法参数直接拒绝,并把错误原因传给模型重新理解。
举一个实际坑:早期我们的查询订单工具没有校验“查询者是否是订单所有人”,结果模型在一次对话里接收了别人的订单号,直接拉出了别人的订单信息。这个漏洞如果上了生产,就是严重的数据泄露事故。后来我加了一道强制校验:工具内部先取当前登录用户 ID,再比对订单归属,不一致直接返回“无权查看”。
走一遍主流平台的自带权限系统也是可以的,但自建应用始终要记住:模型侧认为是“对”的,不代表业务侧应该无条件执行。最终防线在应用本地。
4.4 安全合规提醒
智能体在收集用户消息、订单信息、设备状态时,涉及的隐私数据是相当敏感的。我个人在鸿蒙应用里有两个强制习惯:一是只采集完成业务所必需的最少数据,不把对话上下文无差别上传;二是服务端与模型的通信走可信通道,数据不落明文本地存储。涉及用户敏感指令时,清理界面给出显式说明,不要把用户暴露在不知情的自动操作里。
5. 接入过程中我踩过的坑,以及我的最终选型建议
这部分是我最想分享的,因为文档里基本不会写。
5.1 模型返回的 tool_call 并不总是符合预期的,必须做防御
我遇到过非常多次模型“忘了”按约定格式返回工具调用,或者参数解析失败。一种常见情况是模型把参数放在 content 字段里自然语言输出,而不是放在 tool_calls 里。另一种是模型生成了一个 JSON 字符串,但内部有缺失的引号,直接 JSON.parse 就抛异常。
我的处理方式:解析失败不直接报错,而是把“解析失败”作为一种工具的可调用结果,回传给模型,让模型重新生成格式正确的调用。类似:
json复制{
"success": false,
"error": "tool_call arguments parse error: Unbalanced brackets",
"hint": "请重新生成符合 JSON 格式的 tool_call 参数"
}
实测下来,给模型一次自救机会,比直接中断对话体验好很多。
5.2 流式连接要用心跳,否则长时间对话会莫名断开
鸿蒙端如果我直接开一个长连接做模型流式输出,一旦用户长时间不输入(只是挂机没退出),连接很容易被系统或网络代理断开。我后来加了一个“重连 + 恢复”机制:连接断开后如果检测到还有未完成的 Agent 任务,就自动重连并携带当前上下文继续消费,用户侧基本无感。
5.3 低代码平台接入时的适配问题
我早期也试过在低代码智能体平台上把整套业务动作编排好,然后通过 SDK 接进鸿蒙应用。平台上确实很方便,但碰到一个问题:平台自带的一些预置组件(如订单查询卡片、售后工单表单)和鸿蒙原生的 UI 风格差距很大,做定制适配的成本反而比自建工具路由更高。
所以我的最终建议是:平台负责“大模型大脑”,应用负责“业务手脚”。 复杂决策和自由对话放平台上编排,涉及应用内操作和定制 UI 的部分,全部下沉到鸿蒙本地工具里实现。这样两边各司其职,体验和成本都能兼顾。
5.4 token 成本和响应时长的实测数据参考
以我一直在用的一个中等规模模型为例,实测数据大概是这样的:
| 请求场景 | 平均响应时长 | 单轮 token 消耗 | 工具调用稳定性 |
|---|---|---|---|
| 简单问答,不调用工具 | 0.8 秒 | 约 500 token | 高 |
| 查询订单,调用 1 次工具 | 1.6 秒 | 约 1200 token | 中高 |
| 投诉处理,调用 2 次工具并生成工单 | 2.5 秒 | 约 2200 token | 中 |
工具调用轮次越多,token 消耗越大,响应越长,稳定性也会下降。上线前一定要给自己设定一个“单会话最多工具调用次数”和“单用户会话预算上限”,避免被高频恶意调用打爆账单。
5.5 我目前推荐的一套最小可行架构
如果你现在要在鸿蒙应用里从零开始接智能体,我建议按这个架构起步:
- 客户端(鸿蒙):负责 UI、会话持久化、本地工具执行、通知推送。
- 服务端网关:负责管理 API Key、做用户鉴权、协议转发、限流计费。
- 模型/Agent 平台:负责意图识别、多轮对话、决策与工具调用编排。
- 业务系统:负责订单、售后、CRM 等真实数据接口。
各层之间用标准 JSON 通信,工具描述统一维护在一个配置文件里,应用侧定时拉取,避免客户端和服务端工具定义不一致。
这套结构的好处是:每一层都能独立替换。今天用平台 A 的模型,明天换成本地部署的模型,只需要改网关和客户端路由,不动业务系统,也不动 UI。
最后,关于“AI 办事”这件事我的一点体会
回到开头那句话:别再只做聊天框了。我越来越觉得,智能体在应用里的价值,不在于它说话多像人,而在于它能不能真正把一件事办完。用户说“我要退款”,他不会想听一段“很抱歉您遇到这个问题”的漂亮话,他想看到的是退款申请提交成功、退款进度可查、到账后给他一个准确的通知。这些,靠单一的大模型做不到,必须靠应用和智能体的深度配合。
落地过程中还有一件小事想提醒你:无论你选哪条路线,先找一个很小、很具体的业务场景切入,比如“查订单状态”或者“创建日程提醒”,跑通全链路之后再慢慢加复杂度。一上来就想把智能体做成全能管家,大概率会在工具权限、上下文管理、成本控制这些地方连环踩坑。
我目前这版方案在鸿蒙上已经稳定跑了一段时间,整体体验比原来那个“只会说话的聊天框”好了不止一个量级。你如果也在做类似的事,不妨照着这篇文章的思路先搭一版最小闭环,再逐步把状态管理、任务分发、权限校验这些生产级能力补进来。等到你亲眼看到用户通过智能体完成了一次真实业务操作,你就会明白,这才叫 AI 真正在应用里落了地。
