ITPub博客

首页 > 人工智能 > 人工智能 > 技术解读:HarmonyOS 7 Agent 开发实战 03:Tool Calling 让 AI 查实时订单与库存

技术解读:HarmonyOS 7 Agent 开发实战 03:Tool Calling 让 AI 查实时订单与库存

人工智能 作者:Linux 高手 时间:2026-10-19 10:02:30 0 删除 编辑
头图

前两篇把云端大模型接进了 HarmonyOS 7(API 26),也搭好了 Agent 脚手架和对话 UI。但只要把这套东西拿去做点真实业务,问题立刻暴露:用户问"我的订单到哪了",模型张口就是一句"您的订单正在派送中,请耐心等待"——听着像模像样,其实一个字的真数据都没有。模型根本不知道订单状态,它只是在按语言概率编一个"像答案的答案"。

这就是大模型落地业务系统时最典型的困境: 会说话,但不掌握事实 。本篇用 Tool Calling 解决它:把"查订单""查库存"注册成工具交给模型,模型判断需要时返回一次"调用请求",端侧执行真实查询、把结果回传,模型再基于结果组织最终回答。整套闭环( ToolRegistry + ToolAgent )在一个可开箱即跑的 DevEco 工程里,纯逻辑全部配了不联网就能跑的 Hypium 单测。

一、Tool Calling 到底解决了什么问题

先看没有工具时,想让 AI 答对订单状态只有两条歪路:

  • 把数据塞进提示词 :每轮对话前先查好数据拼进上下文。用户没问订单也得查,token 费用白白涨;问的内容一多,提示词迅速膨胀到失控。

  • 硬编码意图识别 :自己写 if-else 判断"这句话是不是查订单"。换个说法就漏,维护成本指数上升。

Tool Calling 把"什么时候要数据"的决策权交还给模型:请求时随消息带上一份 工具清单 (名称 + 描述 + 参数 schema),模型自己判断该不该调、调哪个、参数填什么。它返回的不是最终答案,而是一个结构化的 tool_calls ;端侧执行完,把结果以 role=tool 消息回传,模型基于真实数据给最终回答。

整个过程是" 要工具 → 本地执行 → 回传 → 再问 "的闭环。一句话总结:模型负责决策与表达,端侧负责事实与执行——各干各的专业的事。

架构图

二、把工具交给模型:ToolDefinition 与 ToolRegistry

每个工具是一对"元信息 + 执行逻辑":元信息(名称、描述、JSON Schema 参数说明)喂给模型做决策,handler 是端侧真实执行的代码。把两者打包成 ToolDefinition ,集中登记进 ToolRegistry :

export
class
ToolDefinition
{
readonly
name
:
string
;
readonly
description
:
string
;
readonly
parameters
:
Object
;
readonly
handler
:
(
args:
Object
) =>
string
;
constructor
(
name:
string
, description:
string
,
              parameters:
Object
, handler: (args:
Object
) =>
string
) {
this
.
name
= name;
this
.
description
= description;
this
.
parameters
= parameters;
this
.
handler
= handler;
  }
toSchema
():
ToolSchema
{
return
{
type
:
'function'
,
function
: {
name
:
this
.
name
,
description
:
this
.
description
,
parameters
:
this
.
parameters
} };
  }
}
export
class
ToolRegistry
{
private
tools
:
Map
<
string
,
ToolDefinition
> =
new
Map
();
register
(
tool
:
ToolDefinition
):
void
{
this
.
tools
.
set
(tool.
name
, tool); }
schemas
():
ToolSchema
[] {
const
out
:
ToolSchema
[] = [];
this
.
tools
.
forEach
(
(
t
) =>
out.
push
(t.
toSchema
()));
return
out;
  }
execute
(
name
:
string
,
args
:
Object
):
string
{
const
tool =
this
.
tools
.
get
(name);
if
(tool ===
undefined
) {
return
JSON
.
stringify
({
error
:
'未知工具: '
+ name });
    }
try
{
return
tool.
handler
(args);
    }
catch
(e) {
return
JSON
.
stringify
({
error
:
'工具执行失败: '
+ (e
as
Error
).
message
});
    }
  }
}

execute 的兜底值得单独说:模型可能调一个没注册的工具名,handler 也可能抛异常(后端超时、参数类型不对)。这两种情况都不该让整条链路崩掉——把错误信息序列化成字符串回传给模型,它大多数时候会修正参数再调一次。 把错误也当成一种"查询结果",是 Tool Calling 稳定性的关键设计。

演示工程里注册了两个工具,handler 用本地 mock 数据(真实环境换成 @ohos.net.http 调你们后端接口即可,编排零改动):

registry.
register
(
new
ToolDefinition
(
'query_order'
,
'查询订单状态与物流信息。参数 order_id:订单号字符串。'
,
  {
type
:
'object'
,
properties
: {
order_id
: {
type
:
'string'
,
description
:
'订单号'
} },
required
: [
'order_id'
] },
  (
args
:
Object
):
string
=>
{
const
id = (args
as
Record
<
string
,
Object
>)[
'order_id'
]
as
string
;
const
db
:
Record
<
string
,
Object
> = {
'20261010'
: {
status
:
'已发货'
,
logistics
:
'顺丰 SF123456'
,
eta
:
'2026-10-12'
}
    };
return
db[id] !==
undefined
?
JSON
.
stringify
(db[id])
                                :
JSON
.
stringify
({
error
:
'订单不存在: '
+ id });
  }
));

三、闭环编排:ToolAgent 的"要工具 → 执行 → 回传 → 再问"

有了注册表,编排层就是把这个闭环跑起来。 ToolAgent.chat() 一次用户输入可能对应多轮模型调用:

async
chat
(
userText
:
string
):
Promise
<
string
> {
this
.
store
.
addUser
(userText);
let
rounds =
0
;
while
(rounds <
this
.
maxRounds
) {
const
req
:
ChatRequest
= {
model
:
this
.
model
,
messages
:
this
.
store
.
getMessages
(),
temperature
:
this
.
temperature
,
tools
:
this
.
registry
.
schemas
()
};
const
resp
:
ChatResponse
=
await
this
.
client
.
chat
(req);
if
(resp.
toolCalls
===
undefined
|| resp.
toolCalls
.
length
===
0
) {
this
.
store
.
addAssistant
(resp.
text
);
return
resp.
text
;
    }
this
.
store
.
addAssistantTools
(resp.
text
, resp.
toolCalls
);
for
(
const
tc
of
resp.
toolCalls
) {
const
result =
this
.
registry
.
execute
(tc.
name
, tc.
parsedArgs
);
this
.
store
.
addToolResult
(tc.
id
, tc.
name
, result);
    }
    rounds++;
  }
const
fallback =
'已多次调用工具仍未得到结论,请稍后重试或联系人工客服。'
;
this
.
store
.
addAssistant
(fallback);
return
fallback;
}
  • maxRounds 上限必须有 。模型偶尔会陷入"调工具→再调工具"的循环,没有上限,一轮对话能把上下文窗口和 token 费用一起打爆。达到上限就返回固定兜底话术,落库收口。

  • 助手消息和工具结果都要落库 。模型要求调工具的那条消息(含 tool_calls )必须原样进历史,紧跟着每条 role=tool 的结果消息——这是协议要求,跳过任何一半,下一轮请求就会被网关拒绝。

  • 每轮都重新带 tools 清单 。工具注册表是动态的,回传历史时清单跟着最新状态走。

四、底座扩展:请求带 tools,响应解析 tool_calls

上一篇的 LlmClient 抽象不用动,只需要两处小扩展。请求侧, buildPayload 在有工具时追加 tools 字段(顺手把温度压低,工具调用要的是稳定不是创意):

if
(req.
tools
!==
undefined
&& req.
tools
.
length
>
0
) {
  payload[
'tools'
] = req.
tools
;
  payload[
'temperature'
] = req.
temperature
??
0.2
;
}

响应侧, ResponseParser 增加对 message.tool_calls 的解析:取 id 、工具名、参数 JSON 串,参数尽量 JSON.parse 成对象,解析失败兜底为 {} (交给 Registry 的错误兜底去处理):

static
extractToolCallsFromMessage
(message):
ToolCall
[] {
const
rawCalls = message[
'tool_calls'
];
if
(rawCalls ===
undefined
|| !
Array
.
isArray
(rawCalls))
return
[];
const
result
:
ToolCall
[] = [];
for
(
const
item
of
rawCalls
as
Array
<
Object
>) {
const
call = item
as
Record
<
string
,
Object
>;
const
fn = call[
'function'
]
as
Record
<
string
,
Object
> |
undefined
;
if
(fn ===
undefined
)
continue
;
const
argsRaw = (fn[
'arguments'
]
as
string
) ??
'{}'
;
let
parsedArgs
:
Object
= {};
try
{ parsedArgs =
JSON
.
parse
(argsRaw); }
catch
(e) { parsedArgs = {}; }
    result.
push
({
id
: (call[
'id'
]
as
string
) ??
''
,
type
:
'function'
,
name
: (fn[
'name'
]
as
string
) ??
''
,
arguments
: argsRaw,
                  parsedArgs });
  }
return
result;
}

解析出的每个 ToolCall 带 id ——它就是回传时的"回执编号"。

五、对话记忆扩展:role=tool 消息落库

ConversationStore 加两个方法,把工具往返原样存进历史:

addAssistantTools
(
text
:
string
,
toolCalls
:
ToolCall
[]):
void
{
this
.
messages
.
push
({
role
:
Role
.
ASSISTANT
,
content
: text, toolCalls });
}
addToolResult
(
toolCallId
:
string
,
name
:
string
,
content
:
string
):
void
{
this
.
messages
.
push
({
role
:
Role
.
TOOL
, content, toolCallId, name });
}

注意 addAssistantTools 和 addToolResult 都 不触发裁剪 :助手消息和它对应的工具结果一旦被裁开,发给模型的序列就不完整了。 Role 枚举相应增加 TOOL = 'tool' 。

六、页面串联与单测

演示页把注册表、 ToolAgent 、UI 串起来。每轮对话结束后从记忆容器同步消息列表,工具结果以中立提示样式呈现——用户能直接看到"AI 调了什么、返回了什么",这比纯文字答案更能建立信任:

private
async
handleSend
():
Promise
<
void
> {
const
text =
this
.
inputText
.
trim
();
if
(text.
length
===
0
||
this
.
sending
)
return
;
this
.
inputText
=
''
;
this
.
sending
=
true
;
try
{
await
this
.
agent
.
chat
(text);
this
.
syncFromStore
();
}
catch
(e) {
}
finally
{
this
.
messages
= [...
this
.
messages
];
this
.
sending
=
false
;
  }
}

单测方面,工具循环用一个 MockToolLlmClient 按序返回"工具调用响应 → 最终答案响应",验证整条链路:解析出 tool_calls → Registry 调度 → role=tool 结果带 tool_call_id 落库 → 第二轮请求把工具结果带回给模型 → 返回最终答案。另有一条反向用例:注册表里故意不注册被调用的工具,验证链路不崩、错误串落库后仍能收口;还有一条验证 maxRounds 兜底。加上解析非法参数 JSON 兜底 {} 、handler 抛错兜底等用例,工程共 27 条断言,全部不发起网络请求。

七、跑起来:三步

  • DevEco Studio 26 打开工程,右键 ohosTest → Run ,确认 27 条断言全绿(也可先 node verify-node.cjs 在命令行验证纯逻辑)。

  • 打开 pages/Index.ets ,把 buildAgent() 里的 baseUrl / apiKey / model 换成自己的网关配置;确认你的云端模型支持 OpenAI 风格的 tools / tool_calls 字段(盘古及主流兼容网关均已支持)。

  • 连真机或模拟器运行,输入"我的订单 20261010 到哪了?"或"SKU001 还有货吗",观察紫色工具提示条里的调用与结果。实际对话表现以真机实测为准。

八、五个高频坑(接 Tool Calling 前先看)

  • 工具描述写得含糊,模型乱填参数 :描述里写清每个参数的名称、类型、格式,最好给示例值;描述是模型的唯一依据。

  • 带 tools 时温度太高 :工具调用要的是稳定可复现,建议 temperature ≤ 0.2 。

  • 工具不存在/执行抛错直接崩链路 :Registry 统一兜底成 JSON 错误串回传,让模型自我修正。

  • tool 结果没按 tool_call_id 回传 :每条 role=tool 消息必须带回引用 id,否则模型无法对齐是哪次调用的结果。

  • 没有 maxRounds 上限 :模型可能反复调工具不收口,设上限兜底,保住上下文和费用。

避坑清单

转载声明:本文为转载整理,原文版权归原作者及原站点所有。

原文来源:Juejin HarmonyOS

原创地址:https://juejin.cn/post/7696758874041188395

原文作者:威哥爱编程;如有侵权,请联系管理员删除。

下一篇: 没有了~
请登录后发表评论 登录
全部评论

注册时间:2020-01-19

  • 博文量
    8
  • 访问量
    2428