茶器艺科智造HarmonyOS应用实战-57-Agent ID和queryText在两处各写一遍,改提示词为何容易只改一半:把智能体配置收回单一来源
茶器艺科智造HarmonyOS应用实战-57-Agent ID和queryText在两处各写一遍,改提示词为何容易只改一半:把智能体配置收回单一来源
HSP 的 ChaqiAgentButton 与 entry 的 ChaqiAgentPage 都声明同一个 Agent ID 和默认 queryText。当前值虽然一致,但“一致”只是这次静态检查的结果,不是工程结构能够持续保证的约束。下一次更换智能体或调整首问时,只要漏改一个入口,同一个产品就会出现两套智能体行为。

本文以 Gitee 仓库 aiding521/chaqi-app-gitee 的 master 分支、提交 f671fcd8de277973bd7518a3c462f68384575f1e 的静态源码为事实基线。本文没有修改项目源码,也没有执行 HAP 构建、模拟器或真机验证;建议代码属于演进方案,不能描述成已经落地的功能。
一、问题不在值错,而在相同事实被写了两遍
静态源码中,ChaqiAgentButton 与 ChaqiAgentPage 都能看到 Agent ID 和默认 queryText。当前代码可以概括为:
const CHAQI_AGENT_ID = 'agent_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
queryText: '请作为茶器设计智能体,给出茶杯造型、釉色和工艺优化建议。'
为避免公开环境标识,本文示例中的 Agent ID 已脱敏;实际工程应从受控配置读取真实值。这个脱敏值不能复制回生产配置,也不能据此推断真实 Agent ID 的格式或内容。
这里最容易产生误判:两处字符串相同,于是评审者会认为“没有问题”。但源码真正暴露的是维护约束缺失——编译器不知道两个字面量必须相等,代码审查也只能靠肉眼比对。一次需求可能只打开页面文件,另一次需求可能只改按钮组件;任何一边单独提交,都有机会留下无声分叉。
二、把风险沿两个入口展开
Agent ID 决定请求所指向的智能体,queryText 决定首次进入时携带的默认问题。二者都不是某个按钮的视觉属性,也不是某个页面独有的交互文案,而是两处入口共享的业务配置。

可以把当前关系看成“两个副本分别喂给两个调用点”:
| 变化 | 只改按钮入口 | 只改页面入口 | 用户可见后果 |
|---|---|---|---|
| 更换 Agent ID | 页面仍走旧值 | 按钮仍走旧值 | 不同入口可能指向不同智能体 |
| 调整 queryText | 页面仍发旧首问 | 按钮仍发旧首问 | 同一能力给出不同上下文 |
| 回滚其中一处 | 两处再次分叉 | 两处再次分叉 | 问题随入口出现,难以复现 |
| 新增第三入口 | 继续复制字面量 | 继续复制字面量 | 同步成本随入口数量增长 |
这些是由重复配置结构推导出的风险,不代表本基线已经在设备上出现了错误回复。本文能确认的是“缺少自动保持一致的机制”,不能把它扩写成“线上已经选错智能体”。
三、单一来源应只承载真正共享的字段
最小收口不是做一套庞大配置中心,而是让 Agent ID 与默认 queryText 只有一个定义位置:
export const CHAQI_AGENT_ID: string = 'agent_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
export const CHAQI_AGENT_DEFAULT_QUERY: string =
'请作为茶器设计智能体,给出茶杯造型、釉色和工艺优化建议。';
这两个导出值表达稳定的茶器智能体业务语义。按钮标题、尺寸、颜色、是否展示以及各自的 FunctionController 仍然属于入口实例,不应因为“集中配置”而被塞进同一个全局对象。判断标准很简单:两个入口是否必须同时遵循这个值?必须一致的才进入共享定义;允许各入口独立变化的继续留在组件内。
还要区分“源码单一来源”和“运行环境配置”。本文只提出消除源码重复的方向,未验证真实 Agent ID 应来自构建配置、受控资源还是其他机制。无论采用哪种来源,都不应把真实标识写进公开文章或日志。
四、两个调用点只消费,不再声明
接回现有 FunctionComponent 时,调用处负责传参,共享模块负责给出值:
FunctionComponent({ agentId: CHAQI_AGENT_ID,
controller: this.controller,
options: { queryText: CHAQI_AGENT_DEFAULT_QUERY }
})
迁移时应分别检查 ChaqiAgentButton 与 ChaqiAgentPage,而不是只让其中一个改用常量、另一个继续保留字面量。完成后的理想搜索结果是:共享定义中出现一次完整默认文案,两个入口只出现常量名;旧 Agent ID 字面量也不应散落在其他调用点。
这种改动的边界很窄。它不要求重写 Controller,不要求合并两个组件,也不要求改变页面路由。把入口结构一起“大一统”反而会把可验证的配置修复扩大成组件重构,增加回归面。
五、用配置变更流程代替人工记忆
这篇的核心流程不是通用任务调度,而是“修改一次,消费多处”:
| 阶段 | 操作 | 应有检查 | 失败时如何定位 |
|---|---|---|---|
| 定义 | 在唯一模块修改 ID 或首问 | 类型与导出名明确 | 直接定位共享定义 |
| 引用 | 两个入口导入同一符号 | 不再声明同义字面量 | 搜索旧字面量和常量名 |
| 审查 | 比较变更范围 | 配置变更只有一个源头 | 发现入口内新增副本即退回 |
| 构建 | 编译各消费模块 | 导入路径和可见性正确 | 记录具体编译错误 |
| 运行 | 分别从两入口触发 | 实际请求配置一致 | 按入口保存日志证据 |
单一来源的价值不是减少两行代码,而是把“一致性”从团队约定变成依赖关系。只要入口直接导入同一导出值,修改者就不必记住另一份副本在哪里。
六、测试应围绕“不会再次分叉”
原有边界用例框架可以保留,但这里的输入和预期要换成配置语义,而不是泛泛测试任意字符串:
interface BoundaryCase<T> {
name: string;
input: T;
expected: string;
}
const cases: BoundaryCase<string>[] = [
{ name: 'empty', input: '', expected: 'reject' },
{ name: 'normal', input: 'valid', expected: 'accept' },
{ name: 'repeat', input: 'valid', expected: 'idempotent' }
];
这段示意代码本身并未证明项目已有对应测试。真正需要覆盖的第一层是静态约束:空配置是否被拒绝、正常配置是否能被两个入口读取、重复读取是否不会改变值。第二层才是编译与运行:两个组件能否正确导入,分别点击后是否使用同一配置。若真实 ID 来自受控环境,还要为“配置缺失”设计明确失败,而不是悄悄回落到文章中的脱敏占位符。
七、不要把 Controller 生命周期混进配置职责
两个入口各自持有的 Controller 或监听仍由各自组件管理。共享常量解决的是配置漂移,不替代资源清理:
private disposeOwnedResources(): void {
// timer/listener只由创建它的组件清理
// 异步结果写状态前核对taskId或pageGeneration
// 可恢复任务先保存业务事实,再释放进程句柄
}
如果后续发现两个入口还重复实现监听或回调处理,应另开问题评估;不能因为这次抽取常量,就默认它们的生命周期也完全相同。配置可以共享,运行时对象通常不能跨页面随意共享。把这两类职责分开,能让本次改动保持最小,也能避免全局 Controller 带来的页面残留问题。
八、验证清单要逐项对应两个入口
- 搜索完整默认 queryText,确认只有共享定义保留字面量。
- 搜索旧 Agent ID;公开记录只保留脱敏形式,真实工程按受控配置核对。
- 检查 ChaqiAgentButton 与 ChaqiAgentPage 都导入相同符号。
- 分别构建包含 HSP 和 entry 消费点的目标,记录结果而不是只写“通过”。
- 分别从按钮与页面入口发起一次操作,核对请求所用配置。
- 修改共享默认首问后重复两入口用例,确认无需第二处同步修改。
- 把构建、模拟器、真机、账号与网络中未执行的部分明确标成“未执行”。
静态搜索能证明字面量是否仍重复,却不能证明真实设备上的 AgentFrameworkKit 调用成功;后者必须有对应运行证据。
九、这类抽取最常见的四种误修
| 误修 | 为什么仍不可靠 | 更合适的方向 |
|---|---|---|
| 两处继续保留,只加注释提醒同步 | 注释不会在漏改时失败 | 让两处引用同一导出值 |
| 只抽取 Agent ID | queryText 仍可能分叉 | 一并识别真正共享的业务配置 |
| 把所有按钮样式也做成全局配置 | 混入允许独立变化的展示属性 | 只集中必须一致的字段 |
| 用脱敏占位符作为运行回退 | 可能把配置错误伪装成正常值 | 配置缺失时明确失败并记录 |
| 看到编译通过就宣称完成 | 未覆盖两个实际入口 | 补充逐入口运行与请求证据 |
“抽常量”看似简单,评审重点却不应是文件少了几行,而应是旧副本是否全部消失、敏感标识是否仍受控、两个入口是否真的共用同一来源。
十、模块落点由依赖方向决定

项目中 entry 负责产品入口,libraryhsp 负责页面和交互,libraryhar 负责模型、算法、路由与通用服务。若 entry 和 libraryhsp 都能依赖承载稳定配置的共享层,常量应放在双方都可消费、又不反向依赖 UI 的位置。最终落点仍需结合仓库现有依赖验证,本文不凭静态片段虚构新的模块关系。
entry / libraryhsp -> 页面、组件、生命周期
libraryhar -> 纯类型、纯函数、可持久化模型
tests -> 边界、幂等、恢复序列
runtime evidence -> 日志、设备行为、服务读回
关键约束是依赖只能从入口指向共享定义,共享层不能为了拿到页面状态反过来导入 entry 或 HSP。若现有依赖不允许两端共同引用,就应先确认一个合法的共享位置,而不是复制第三份常量来绕过编译问题。
十一、基线证据与结论边界
baseline: f671fcd8de277973bd7518a3c462f68384575f1e
scope: source-level proposal
build: not run
simulator/device: not run
network/account: not run
result: static boundary identified; implementation pending
当前证据只支持三个结论:基线提交中有两个入口、两处声明当前相同、这种结构依赖人工同步。它不支持“改造已经完成”,也不支持“真实账号调用已经验证”。后续若源码、SDK 或产品规则变化,需要重新固定仓库与 commit,再复查定义和消费点。
十二、把回归步骤写成可复现的双入口场景
验证不应停在“全局搜索只有一个结果”。最小场景是先记录共享配置基线,再分别进入 ChaqiAgentButton 与 ChaqiAgentPage,观察两条路径是否消费同一配置;然后只修改共享默认 queryText,重新构建并重复两条路径。整个过程中不应再编辑任一入口中的文案副本。
| 场景 | 操作序列 | 必须观察的证据 | 不能替代的证据 |
|---|---|---|---|
| 基线搜索 | 搜 ID 与完整首问 | 定义点、消费点列表 | 只看单个文件 |
| 按钮入口 | 从按钮发起智能体能力 | 所用配置与终态日志 | 按钮能被点击 |
| 页面入口 | 直接进入智能体页面 | 所用配置与终态日志 | 页面能正常显示 |
| 单点修改 | 只改共享 queryText 后重测 | 两入口同时取得新值 | 人工比对两份字符串 |
| 配置缺失 | 移除受控值后启动 | 明确错误与恢复入口 | 自动使用脱敏占位符 |
异步调用若需要追踪,可以继续使用带身份和代次的记录结构,但它属于运行证据,不应反过来承担配置同步:
interface OperationTrace {
taskId: string;
generation: number;
startedAtMs: number;
finishedAtMs?: number;
terminalSource?: 'ui' | 'service' | 'restore' | 'cancel';
}
function canApplyResult(trace: OperationTrace, currentGeneration: number): boolean {
return trace.generation === currentGeneration &&
trace.finishedAtMs === undefined;
}
日志可以记录入口名、配置版本摘要、任务身份与终态来源,但不应记录真实 Agent ID、令牌或完整用户输入。这样既能比较两个入口,又不会用“可观测性”制造新的敏感信息泄露点。
本篇的收口标准因此很具体:共享事实只定义一次;两个入口都直接消费;脱敏值不进入真实运行回退;验证按入口分别取证。本文尚未执行这些改动和运行步骤,当前结论仍是源码级演进建议。
更多推荐



所有评论(0)