【共创稿事节】HarmonyOS 7 Agent 开发实战 11:并发多会话隔离
02 篇的ConversationStore解决的是「一场对话怎么存」。但真实 App 从来不止一场对话用户可能在 A 会话问报销、在 B 会话查订单后台可能同时跑着一个定时摘要任务和一个前台问答。如果所有会话共用一个ConversationStoreA 问的「密码」会被 B 的上下文读到B 的订单号会污染 A 的记忆——这就是上下文串台。串台最麻烦的地方在于它不报错模型照样返回一段像模像样的话只是答案用错了上下文。等到用户发现「我明明没提过这个订单号」排查成本已经很高了。11 这一篇给出根治方案每个会话一套独立的ConversationStoreAgent由SessionManager统一登记、隔离、淘汰。三种隔离粒度该隔离到哪一层隔离不是越彻底越好要在「正确性」与「开销」之间取平衡隔离粒度做法能防住什么代价只隔离消息每会话一个ConversationStore共用一个Agent历史消息不串台防不住——Agent内的工具循环、流式累积状态仍会跨会话污染隔离 store Agent本文每会话一套ConversationStoreAgentLlmClient消息与编排状态都不串台会话多时对象数量上升需要容量淘汰隔离到线程每会话一个 Worker连并发调度都物理隔离线程开销大适合少量长任务而非普通对话结论很直接绝大多数 App 做到第二档就够了。第一档是常见的「看起来做了其实没做」——消息是不串了但 03 的工具循环、05 的流式累积状态还挂在共用的Agent上照样会串。第三档留给后台长任务普通聊天会话用不上。隔离的最小单元SessionEntry隔离的本质是「每个会话持有自己的状态」而不是「每次调用传一个 sessionId 进去筛选」。前者是状态隔离后者是查询过滤——后者只要有一处忘记传 id串台就发生了。// 单条会话的隔离单元每条会话持有自己独立的 ConversationStore 与 Agent// 消息在内存中互不串台。lastAccess 用单调递增计数器排序保证 LRU 与时钟精度无关。exportclassSessionEntry{readonlysessionId:string;readonlystore:ConversationStore;// 该会话私有的对话记忆readonlyagent:Agent;// 该会话私有的编排实例lastAccess:number;// 越大表示越近被访问constructor(sessionId:string,client:LlmClient,systemPrompt:string,model:string){this.sessionIdsessionId;this.storenewConversationStore(systemPrompt);this.agentnewAgent(client,systemPrompt,model);this.lastAccess0;}// 该会话已落库不含系统消息的对话条数getmessageCount():number{returnthis.store.size();}}三个字段各自独立缺一不可store私有消息落进自己的数组getConversation()拿到的永远是本会话的。agent私有Agent内部持有LlmClient与编排状态如 03 的工具循环、05 的流式累积这些也必须按会话隔离否则 A 会话的工具调用会写进 B 的循环里。clientFactory逐会话生成不同会话可能需要不同的模型或密钥比如付费用户走大模型、免费用户走小模型所以客户端由工厂按sessionId现造而不是全局共用一个。SessionManager登记、取用、淘汰管理器用Mapstring, SessionEntry做索引对外只有三个动作取没有就建、发消息、查数量。// 多会话管理器用 Map 隔离各会话上下文并基于访问顺序做 LRU 淘汰。// 纯逻辑、可测真正的并行执行交给 TaskPool/Worker见文章说明。exportclassSessionManager{privatereadonlysessions:Mapstring,SessionEntrynewMapstring,SessionEntry();privateclock:number0;// 单调递增访问计数器privateevictedTotal:number0;privatenow():number{this.clock;returnthis.clock;}// 取已有会话并刷新访问时间不存在则新建必要时先淘汰最久未访问者getOrCreate(sessionId:string):SessionEntry{constexisting:SessionEntry|undefinedthis.sessions.get(sessionId);if(existing!undefined){existing.lastAccessthis.now();returnexisting;}// 容量边界即将超过上限时先淘汰 LRU再放进新会话if(this.sessions.sizethis.maxSessions){this.evictLeastRecentlyUsed();}constclient:LlmClientthis.clientFactory(sessionId);constentry:SessionEntrynewSessionEntry(sessionId,client,this.systemPrompt,this.model);entry.lastAccessthis.now();this.sessions.set(sessionId,entry);returnentry;}// 委派给该会话自己的 Agent仅影响本会话的消息计数asyncsend(sessionId:string,userText:string):Promisestring{constentry:SessionEntrythis.getOrCreate(sessionId);entry.lastAccessthis.now();returnawaitentry.agent.send(userText);}}为什么 LRU 用单调计数器而不是 Date.now()这是本文最容易被忽略、但上线后一定会踩的坑。如果用Date.now()记录lastAccess同一毫秒内访问两个会话时两者的时间戳完全相同LRU 选出谁取决于Map的遍历顺序结果不可预测系统时钟可能被调整NTP 校时、用户改时间时间戳甚至可能回退导致「刚访问过的会话反而被判为最旧」。改成单调递增计数器就彻底规避了这两点谁先被访问谁的数字小同毫秒访问也有严格先后且与真实时钟无关。代价只是这个数字不能跨进程持久化——但 LRU 本来就是内存态策略重启动后重建即可。淘汰发生在「插入前」而不是「插入后」注意getOrCreate里的顺序先判断size maxSessions淘汰一个再放进新的。这样size()在任何时刻都不会超过maxSessions。如果写反先插后淘汰容量就会短暂超限一瞬而这一瞬恰好是内存峰值最危险的时候。privateevictLeastRecentlyUsed():void{letoldestId:string|undefinedundefined;letoldestTime:numberNumber.MAX_SAFE_INTEGER;for(const[id,entry]ofthis.sessions){if(entry.lastAccessoldestTime){oldestTimeentry.lastAccess;oldestIdid;}}if(oldestId!undefined){this.sessions.delete(oldestId);this.evictedTotal;}}真机上的「并发」到底在哪一层需要说清楚一个边界本文的SessionManager解决的是状态隔离它本身是纯逻辑、单线程安全的。真正的并行比如同时向模型发三个请求、互不阻塞要靠 HarmonyOS 的并发设施TaskPool适合「一次性、无状态、可并发」的任务。把每个会话的一次 LLM 调用包装成Concurrent函数丢进任务池系统按负载调度。任务函数不能捕获外部变量入参出参需可序列化——这恰好和「会话状态由SessionEntry持有、任务只带 id 和文本」的设计契合。Worker适合「长时间、有状态」的后台线程比如一个持续运行的定时摘要会话。两者都遵循同一条纪律共享的可变状态不要跨线程直接读写。所以SessionEntry里的store只由持有它的那个线程/任务更新管理器本身只在主线程做登记与淘汰。这样隔离逻辑既能单测迁移到真并发时也不用推倒重来。落到代码上TaskPool的任务函数只带可序列化的入参不捕获外部状态真正的落库仍回到持有方// 并发任务只带可序列化的入参不捕获外部状态ConcurrentfunctioncallLlmOnce(sessionId:string,userText:string):string{// 注意这里拿不到主线程的 SessionEntry只做一次无状态调用returnsessionId:userText;}// 主线程任务只负责跑状态更新回到持有方consttask:taskpool.Tasknewtaskpool.Task(callLlmOnce,A,查一下订单);constraw:stringawaittaskpool.execute(task)asstring;constreply:stringawaitmanager.send(A,查一下订单);// 落库仍在持有方完成这里的分工值得强调TaskPool负责「把一次耗时的网络调用丢出去并发跑」SessionManager负责「这次调用的结果该记到谁的账上」。把并发与归属拆开两者才能各自独立演进——换并发框架不影响隔离结构改隔离策略也不影响并发代码。演示页两个会话互不相干entry/src/main/ets/pages/Index.ets用MockLlmClient离线演示两个输入框分别绑定 sessionA和 sessionB各自发送后页面实时显示各自的消息条数。往 A 发 3 条、往 B 发 1 条A 显示 63 问 3 答、B 显示 2——互不干扰。「创建超出容量的会话」按钮会连续创建超过maxSessions的会话页面显示evictedTotalCount()增长证明最久未访问者被淘汰。测试隔离要能被断言隔离这种事肉眼在界面上很难证明必须落到断言隔离性往 A 发消息后messageCount(A)增加而messageCount(B)不变且A.store.getConversation()里不含 B 的文本。getOrCreate幂等同一 id 连续取两次返回同一个SessionEntry可用引用相等或消息计数断言。LRU 顺序maxSessions 2时依次访问 A、B再访问 A然后创建 C —— 被淘汰的应是 B 而不是 A。容量边界创建恰好maxSessions个不触发淘汰再创建第maxSessions 1个时evictedTotal变成 1。verify-node.cjs用纯 JS 镜像了同样的逻辑命令行 20 条断言全绿entry/src/ohosTest/ets/test/LlmClient.test.ets另有 Hypium 断言含 01/02 底座的复用部分不装 DevEco 也能先验纯逻辑。避坑六个常见错误只隔离消息不隔离 Agent。共用一个Agent实例时03 的工具循环、05 的流式累积状态会跨会话污染。对策连Agent乃至LlmClient一起按会话私有化。用Date.now()做 LRU 时间戳。同毫秒并列 时钟回退会让淘汰结果不可预测。对策改用单调递增计数器。先插入后淘汰。容量会短暂超限。对策插入前先判边界并淘汰。把sessionId当查询条件而不是隔离键。只要有一处漏传 id 就会串台。对策状态按会话持有别用「全局表 过滤」的写法。跨线程共享可变状态。TaskPool任务里直接改主线程的ConversationStore结果取决于调度时序。对策任务只带 id 与文本状态更新回到持有方。淘汰时忘了释放资源。会话里若持有流式连接、定时器淘汰前要显式关闭否则会泄漏。对策给SessionEntry加dispose()淘汰时调用。参考与出处HarmonyOS ArkTS 语言TaskPool 并发任务概述Worker 多线程概述Hypium 自动化测试框架使用指南最后一句隔离不是把 id 传下去而是让每个会话从一开始就拥有自己的一份状态——串台这种 bug靠约定防不住只能靠结构。

相关新闻

【RH850U2A芯片】异常分析和定位

【RH850U2A芯片】异常分析和定位

目录 前言 正文 1.Trap类型 1.1.软件工程中的三类trap 1.2.手册中的三类Trap 1.3.异常原因及如何定位异常位置 1.3.1.通用定位套路(先做这一步) 1.3.2.各 Exception:触发原因 → 代码问题怎么找 2.和TC3xx芯片的寄存器对照&#xff…

2026/10/18 5:46:43 阅读更多 →
AI应用架构师的伦理设计指南:五大方向拆解与落地实践

AI应用架构师的伦理设计指南:五大方向拆解与落地实践

AI伦理设计这几年已经从“论文里的概念”变成了“架构图上的硬约束”。作为AI应用架构师,我感触特别深:以前做项目,大家关心的是模型精度够不够、性能能不能撑住,现在客户和业务方开口问的往往是“这个模型会不会歧视某个群体”“…

2026/10/18 5:46:43 阅读更多 →
从“1.31完成56、63、60”看版本收口:从编号到交付的完整路径

从“1.31完成56、63、60”看版本收口:从编号到交付的完整路径

项目记录里躺着这么一行:“1.31完成56、63、60”。没有正文、没有解释、没有关联链接。如果你在一个开发团队里待过,大概率见过类似写法——日期、几个编号、一个结束语,看起来像流水账。但这行字恰恰是版本收口时最典型的缩影:1月…

2026/10/18 5:46:43 阅读更多 →

最新新闻

hostapd-2.10源码解析:从编译到二次开发实战指南

hostapd-2.10源码解析:从编译到二次开发实战指南

简介:hostapd-2.10源码包面向Linux无线开发与嵌入式网络工程师,用于在Linux系统上以软件方式创建和管理WiFi热点,替代硬件AP实现主机模式接入点。包内共655个文件,以363个C源文件和235个头文件为核心,覆盖nl80211驱动、…

2026/10/18 15:50:15 阅读更多 →
LISREL验证性因素分析完整指南:从模型识别到拟合判读

LISREL验证性因素分析完整指南:从模型识别到拟合判读

简介:一份面向社会科学、心理学及教育学研究者与学生的LISREL软件验证性因素分析教学PPT,系统讲解结构方程模型中验证性因素分析的核心逻辑与实操要点。资源从“准确Vs简洁”的数据处理两难切入,说明如何输入相关矩阵、设定理论模型、输出再生…

2026/10/18 15:50:15 阅读更多 →
通义千问Qwen3本地部署实战:MoE架构、硬件选型与性能调优

通义千问Qwen3本地部署实战:MoE架构、硬件选型与性能调优

前几天有个朋友来问我,说他拿到一台旧工作站,想跑一个新出的开源模型,问我到底值不值得折腾。我第一反应是问他:你要跑的模型是哪个?他回答说在想跑通义千问 Qwen3 系列。我说那就很值得,Qwen3 这一代把 Mo…

2026/10/18 15:50:15 阅读更多 →
AI原生应用人机共创成功案例:用TaoToken统一Key打通多模型协作链路

AI原生应用人机共创成功案例:用TaoToken统一Key打通多模型协作链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 15:50:15 阅读更多 →
翁恺C语言习题集刷题指南:环境搭建、题型拆解与避坑要点

翁恺C语言习题集刷题指南:环境搭建、题型拆解与避坑要点

简介:浙江大学翁恺教授C语言课程习题与源码合集,专门面向C语言初学者、备考计算机等级考试或自学编程的读者,覆盖从基础语法到典型算法的多层次练习。资源共4个文件,包含txt题解文档、Python辅助脚本、inscode在线交互文件和html页…

2026/10/18 15:50:15 阅读更多 →
红色党建风工作总结PPT模板:从空白页到成稿的工程化流程

红色党建风工作总结PPT模板:从空白页到成稿的工程化流程

简介:这是一套面向党政部门的工作总结PPT模板,整体采用红色党建风格,视觉庄重且富有感染力,适合年终总结、阶段汇报、主题教育展示等场合。模板内置完整汇报逻辑,共分为工作目标与背景、工作成果与亮点、挑战与解决方案…

2026/10/18 15:49:14 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →