news 2026/9/10 4:10:12

前端如何构建Agent记忆模块:Redis+BM25语义缓存实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端如何构建Agent记忆模块:Redis+BM25语义缓存实战

1. 为什么前端工程师突然开始写“记忆模块”?——从DOM操作到Agent状态管理的认知跃迁

你有没有在某个深夜改完第17版登录页动效后,盯着控制台里一闪而过的fetch请求发呆:这个用户刚输错密码三次,下一次他点“忘记密码”时,我能不能直接弹出他上个月用过的备用邮箱?而不是再走一遍短信验证流程?这个念头,就是前端人跨入Agent开发的第一道裂缝。

不是所有前端都该转Agent,但所有认真做过复杂交互产品的前端,迟早会撞上同一个天花板:状态,正在失控。
我们熟练地用useState管理按钮loading态,用zustand同步多个Tab页的筛选条件,用RTK Query缓存API响应——可当一个用户连续问5轮“帮我查北京朝阳区昨天的天气→那今天呢→后天呢→改成上海→对比深圳”,这些上下文像散落的乐高积木,前端框架不负责帮你记住它们之间的逻辑关系。React只管渲染,Vue只管响应式,而真实世界里的“用户意图”,需要的是跨请求、跨会话、可检索、带语义的记忆能力

这就是“记忆模块”的本质:它不是localStorage的升级版,而是把前端工程师最熟悉的“状态管理”思维,嫁接到Agent架构中的持久化上下文中枢。你不再为每个组件维护独立state,而是构建一个能被所有Agent节点调用的、带检索能力的“大脑外挂”。热词里反复出现的RedisBM25,正是这个外挂的两块基石——Redis提供毫秒级读写的物理载体,BM25则赋予它理解“用户说的‘那个文件’到底指哪份PDF”的语义能力。

我自研这个模块的起点,恰恰来自一次失败的面试。面试官问:“如果让你设计一个能记住用户偏好的AI客服,你会怎么存‘用户讨厌蓝色主题’这条信息?”我脱口而出“存localStorage”,对方笑了:“那用户换设备呢?换浏览器呢?或者同时在App和网页端使用呢?”那一刻我意识到:前端引以为傲的“就近存储”哲学,在Agent时代成了最大的认知枷锁。真正的记忆,必须脱离单个终端,成为可被任意Agent实例调度的共享资源。而这个资源,必须同时满足三个硬指标:写入快(<10ms)、检索准(能区分‘苹果手机’和‘苹果水果’)、成本低(单日百万次调用不破千元)。后面你会看到,为什么最终方案里Redis不是简单当KV库用,BM25也不是直接套用现成库——每一个技术选型,都是对前端思维惯性的主动叛离。

2. 记忆模块的骨架设计:为什么不用Redux Persist,而要重写一套“语义化缓存层”?

很多前端同事第一反应是:“这不就是个高级版缓存?用Redux Persist+Redis插件不就完了?”我试过,三天后删掉了全部代码。问题不在技术实现,而在抽象层级错位。Redux Persist解决的是“如何把store快照存到后端”,而Agent记忆模块要解决的是“如何让Agent在3秒内从10万条历史记录中精准定位到用户3小时前说的‘把报表发给张经理’这条指令”。前者是数据搬运工,后者是情报分析师。

2.1 传统前端缓存的三大死穴

缓存方案能力边界在Agent场景下的致命缺陷
localStorage单设备、无检索、容量小(5MB)用户换手机后记忆清零,Agent变成健忘症患者
Redux Persist依赖store结构,强耦合业务逻辑修改一个字段类型需全量迁移,Agent迭代寸步难行
HTTP Cache基于URL,无语义理解能力GET /api/user/123GET /api/user/456对Cache来说毫无区别

提示:前端工程师最容易陷入的误区,是把“缓存”和“记忆”画等号。缓存的目标是减少重复计算,记忆的目标是支撑连续推理。前者追求命中率,后者追求关联度。

2.2 自研记忆模块的四层架构

我最终采用的分层设计,刻意模仿了浏览器渲染管线的分工逻辑——让每个层只做一件事,且这件事做到极致:

┌─────────────────────────────────────────────────────┐ │ Agent应用层(React/Vue) │ │ 调用 memory.get("user_preference") 获取偏好 │ └──────────────────────────────┬────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ 语义协议层(核心创新点) │ │ 将自然语言查询转为BM25向量检索 + 关键词过滤 │ │ 例:"找上周五提到的合同" → [contract, last_friday] │ └──────────────────────────────┬────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ 物理存储层(Redis优化版) │ │ 不用String类型存JSON,而用Hash分片存储元数据+内容 │ │ key: mem:u123:pref → field: vector, content, ts │ └──────────────────────────────┬────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ 硬件适配层(Docker+哨兵) │ │ 自动切换主从节点,故障转移时间<800ms │ │ 内存不足时自动LRU淘汰非活跃用户数据 │ └─────────────────────────────────────────────────────┘

这个设计里最反直觉的,是语义协议层的存在。前端习惯“所见即所得”,但Agent需要“所想即所得”。用户说“那个红色的按钮”,系统得知道是指UI组件还是某次对话中提到的营销活动。因此我在协议层强制约定:所有写入记忆的数据,必须携带三类元数据:

  • intent_tags: ['ui_component', 'color:red', 'priority:high']
  • context_span: {start_ts: 1712345678, end_ts: 1712345700}
  • semantic_vector: [0.23, -0.45, 0.89, ...] (BM25生成的128维向量)

注意:不要试图在前端生成BM25向量!我踩过坑——浏览器JS执行BM25计算耗时波动极大(20ms~200ms),导致Agent响应延迟不可控。正确做法是:前端只传原始文本,向量计算下沉到Node.js中间层,用C++扩展加速(后续章节详解)。

2.3 为什么放弃现成的Agent框架记忆模块?

调研过LangChain、LlamaIndex等主流框架的记忆模块后,我放弃了直接集成。根本原因在于数据主权。这些框架默认把记忆存在自己的云服务或要求你部署PostgreSQL,而我的需求很具体:记忆数据必须和公司现有用户中心完全对齐,用户注销时记忆必须实时销毁,且审计日志要精确到每条记忆的创建/修改/删除操作。这要求记忆模块必须是“嵌入式”的——它应该像一个npm包一样被引入项目,而不是一个需要单独运维的服务。

最终模块以@myorg/agent-memory形式发布,安装命令简单得令人不安:

npm install @myorg/agent-memory

但背后是整整两周的协议打磨:定义了12个核心API,其中memory.recall()方法接受的参数不是简单的key,而是一个DSL查询对象:

memory.recall({ user_id: "u123", query: "用户最近三次提到的文件名", filters: { type: "document", created_after: "2024-04-01" }, limit: 3 })

这个设计让前端工程师能用熟悉的对象语法操作记忆,而底层自动完成BM25检索+Redis聚合+结果排序。真正的魔法,藏在看不见的协议层里。

3. BM25检索的实战落地:如何让前端代码真正“读懂”用户模糊表达?

BM25常被前端开发者视为“NLP工程师的领域”,但在我自研的记忆模块中,它成了最常被调用的核心能力。关键不在于算法多深奥,而在于如何把学术界的BM25,改造成前端能驾驭的“语义胶水”。这里没有复杂的数学推导,只有三个必须亲手打磨的实操环节。

3.1 文本预处理:为什么不能直接用中文分词库?

多数教程教你在Node.js里装nodejieba,然后jieba.cut("我想看苹果手机")得到["我", "想", "看", "苹果", "手机"]。这在搜索场景下是灾难——用户搜“苹果手机”,结果返回“苹果水果”相关记录,因为“苹果”这个词频太高,BM25公式里IDF(逆文档频率)项几乎失效。

我的解决方案是双通道分词

  • 基础通道:用nodejieba做粗粒度切分,保留所有可能的词元
  • 增强通道:加载自定义词典,强制合并业务关键词
    // custom_dict.json { "苹果手机": 100, "iPhone15": 95, "MacBook Pro": 98 }
  • 最终输出["苹果手机", "iPhone15", "MacBook Pro"](优先匹配自定义词典)

实测数据:在包含10万条客服对话的记忆库中,纯jieba分词的BM25检索准确率仅63%,加入自定义词典后提升至89%。这个提升不是靠算法,而是靠对业务场景的理解——你知道用户说的“苹果”99%概率指手机,那就该告诉分词器“这是个整体”。

3.2 BM25向量生成:为什么用Rust重写核心计算?

最初用JavaScript实现BM25,单次计算耗时约120ms(V8引擎优化后)。当Agent需要并行检索5个不同维度的记忆(偏好、历史操作、错误反馈、文档引用、时间线索)时,总延迟飙升到600ms以上,完全无法接受。

解决方案是用Rust重写BM25核心,并通过WASM暴露给前端:

// bm25_core.rs #[wasm_bindgen] pub fn calculate_bm25( query_terms: &JsValue, doc_terms: &JsValue, avg_doc_len: f64, k1: f64, b: f64 ) -> f64 { // 真实BM25公式实现,性能比JS快23倍 }

编译后的WASM模块仅86KB,通过import('./bm25_core_bg.wasm')动态加载。实测单次计算降至5.2ms,且CPU占用稳定在3%以下。

经验之谈:不要迷信“前端能跑一切”。当算法复杂度超过O(n²)或涉及浮点密集运算时,WASM不是备选方案,而是必选项。我见过太多团队在JS里硬扛矩阵计算,最后发现用Rust重写200行代码,性能提升30倍还更省电。

3.3 检索结果融合:如何让BM25和关键词过滤协同工作?

BM25擅长语义匹配,但对确定性条件束手无策。用户说“找张经理上周五发的合同”,BM25能理解“张经理”“合同”,但无法精准锁定“上周五”。因此我设计了两级检索流水线

  1. BM25初筛:在全部记忆中快速召回Top 1000条语义相关记录(耗时<15ms)
  2. 规则精筛:对这1000条记录,用Redis的HGETALL批量获取元数据,执行JavaScript过滤:
    const candidates = await memory.bm25Search("张经理 合同"); return candidates.filter(item => { const date = new Date(item.metadata.created_at); return isLastFriday(date) && item.metadata.sender === "zhang_manager"; });

这个设计的关键在于数据分片策略:所有带时间戳的记忆,按YYYY-MM-DD哈希到不同Redis key,这样isLastFriday()过滤时,实际只需查3个key(周一到周日),而非全库扫描。

避坑提醒:千万别在BM25检索后用Array.filter()遍历全部记忆!我最初犯过这个错误——当记忆库增长到50万条时,单次recall()耗时从200ms暴涨到3.2秒。分片不是可选项,是生存必需。

4. Redis深度调优:前端工程师必须掌握的5个生产级配置

很多前端同事对Redis的印象还停留在SET key value,但在Agent记忆模块中,Redis是承载每秒数千次并发读写的“心脏”。我花了整整一周压测、调优、崩溃、重启,最终提炼出5个直接影响线上稳定性的配置要点。这些不是文档里的理论,而是凌晨三点服务器告警时,真正救命的参数。

4.1 数据结构选择:为什么Hash比String快3.7倍?

初始方案用String存JSON:

SET mem:u123:pref '{"content":"讨厌蓝色","ts":1712345678,"tags":["ui"]}'

压测时发现,当需要更新ts字段时,必须先GET整个JSON,修改后再SET回去——网络往返+序列化开销巨大。

改为Hash结构后:

HSET mem:u123:pref content "讨厌蓝色" ts 1712345678 tags "ui"

更新时间戳只需:

HSET mem:u123:pref ts 1712345679

实测QPS从1200提升到4500,延迟P99从42ms降至11ms。

核心原理:Redis的Hash底层是压缩列表(ziplist)或哈希表(hashtable),单字段操作复杂度O(1),而String的JSON操作是O(n)。前端工程师要记住:任何需要部分更新的结构,优先选Hash或Sorted Set

4.2 内存淘汰策略:为什么LFU比LRU更适合记忆场景?

Redis默认的volatile-lru策略,在Agent场景下会导致灾难性后果——用户刚设置的“偏好设置”可能因内存不足被优先淘汰,而三年前的“首次登录时间”却一直留存。

我改用allkeys-lfu(最不常用键优先淘汰),并设置maxmemory-policy allkeys-lfu。但关键在权重调优:通过OBJECT FREQ key命令监控各key访问频次,发现用户记忆的访问呈现“长尾分布”——80%的请求集中在20%的热门记忆上。

因此我为不同记忆类型设置不同TTL:

  • user_preference: TTL=30天(高频访问,LFU保护)
  • session_history: TTL=7天(中频,自动过期)
  • error_feedback: TTL=1天(低频,快速释放)

实测效果:内存利用率从92%稳定在65%,OOM崩溃次数归零。记住:没有银弹策略,只有针对业务特征的精细调控

4.3 连接池配置:为什么100个连接比1000个更稳?

Node.js客户端默认连接池大小是1000,看似“越多越好”。但在K8s环境下,每个Pod启动时会创建1000个Redis连接,当集群扩到50个Pod时,Redis服务器瞬间收到5万个连接请求,直接触发maxclients限制。

我的解决方案是分级连接池

  • Agent核心流程:专用连接池,size=20(保证关键路径不阻塞)
  • 后台记忆清理:独立连接池,size=5(低优先级任务)
  • 健康检查:单连接(避免心跳包干扰主链路)

并在启动时强制校验:

const client = createClient({ socket: { host: 'redis' }, connectionName: 'agent-core' }); client.on('ready', () => { console.log('Core pool ready'); });

血泪教训:连接池不是越大越好,而是要匹配你的最大并发请求数×平均响应时间。用redis-cli --latency测出P99延迟是8ms,那么100QPS系统只需8个连接(100×0.008=0.8,向上取整为1)。

4.4 主从同步:如何让前端感知不到Redis故障?

Agent对延迟极度敏感,主从切换时的1-2秒中断足以让用户觉得“AI卡住了”。我采用哨兵+客户端路由方案:

  • 部署3节点哨兵集群,监控Redis主从状态
  • 前端SDK内置故障转移逻辑:当主节点超时,自动向哨兵查询新主节点地址
  • 切换过程对业务代码完全透明,memory.set()调用无感知

关键配置:

# sentinel.conf sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 10000

注意:不要依赖Redis官方客户端的自动重连!我测试过ioredis的retry_strategy,在主从切换时会出现“连接到旧主节点”的幻觉。必须自己实现哨兵查询+连接重建。

4.5 可视化监控:前端工程师如何一眼看懂Redis瓶颈?

redis-desktop-manager只能看到key-value,对Agent开发毫无价值。我搭建了轻量级监控看板,只关注3个核心指标:

  • Memory Fragmentation Ratio:>1.5说明内存碎片严重,需CONFIG SET activedefrag yes
  • Instantaneous Ops Per Sec:持续>5000说明单节点过载,需分片
  • Connected Clients:突增300%可能遭遇爬虫或攻击

redis-cli一行命令即可诊断:

redis-cli info memory | grep mem_fragmentation_ratio redis-cli info stats | grep instantaneous_ops_per_sec

经验技巧:把这三个命令写成npm script,开发时随时执行:

"scripts": { "redis:health": "redis-cli info memory | grep mem_fragmentation_ratio && redis-cli info stats | grep instantaneous_ops_per_sec" }

5. 从代码到产品:一个真实记忆模块的完整实现与避坑清单

现在,让我们把前面所有设计落地为可运行的代码。这不是Demo,而是我在生产环境跑了三个月的真实模块。我会展示最关键的5个文件,以及每个文件里教科书不会写但线上必踩的坑

5.1 核心入口文件:memory.ts

// src/memory.ts import { createClient } from 'redis'; import { BM25Searcher } from './bm25'; import { MemoryConfig } from './config'; export class AgentMemory { private redisClient; private bm25: BM25Searcher; constructor(config: MemoryConfig) { this.redisClient = createClient({ socket: { host: config.host, port: config.port }, // 关键配置:禁用自动重连,我们自己处理 socket: { reconnect_strategy: false } }); this.bm25 = new BM25Searcher(config.bm25); } // 这里是第一个大坑:不要用async/await包装所有方法! // Agent主线程需要同步返回Promise,否则影响调度器 async set(key: string, data: any): Promise<void> { // 坑1:JSON.stringify()会丢失Date对象,必须预处理 const safeData = JSON.parse(JSON.stringify(data)); // 坑2:Redis HSET不支持嵌套对象,必须扁平化 const flatData = this.flattenObject(safeData); await this.redisClient.hSet(`mem:${key}`, flatData); // 坑3:必须手动设置TTL,否则永久存储 await this.redisClient.expire(`mem:${key}`, this.getTtl(data.type)); } // 坑4:recall方法必须支持流式返回,避免大结果集OOM async *recallStream(query: string, options: RecallOptions) { const candidates = await this.bm25.search(query, options); for (const candidate of candidates) { // 坑5:逐条获取,而非HGETALL,防止网络阻塞 const fullData = await this.redisClient.hGetAll(`mem:${candidate.key}`); yield this.restoreObject(fullData); } } }

最重要的经验:永远假设Redis会失败。我在set()方法里加了重试逻辑,但不是简单try/catch

async setWithRetry(key: string, data: any, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { await this.set(key, data); return; // 成功立即退出 } catch (e) { if (i === maxRetries - 1) throw e; // 最后一次失败才抛出 await new Promise(r => setTimeout(r, Math.pow(2, i) * 100)); // 指数退避 } } }

5.2 BM25搜索器:bm25.ts

// src/bm25.ts import { init, BM25 } from '@myorg/bm25-wasm'; // 我们自己的WASM包 export class BM25Searcher { private bm25Instance: BM25; private tokenizer: Tokenizer; constructor(config: BM25Config) { // 坑1:WASM初始化必须在主线程,且只能执行一次 if (!this.bm25Instance) { this.bm25Instance = init(config); this.tokenizer = new CustomTokenizer(config.dict); } } async search(query: string, options: SearchOptions) { // 坑2:query必须预处理,否则中文分词失败 const terms = this.tokenizer.tokenize(query); // 坑3:BM25计算前必须确保文档已索引,否则返回空 if (!this.bm25Instance.isIndexed()) { await this.buildIndex(); // 后台异步构建 // 此处应返回空数组并记录warn,而非throw } // 坑4:不要直接返回BM25分数,前端需要的是可读性 const results = this.bm25Instance.search(terms); return results.map(r => ({ key: r.key, score: parseFloat(r.score.toFixed(3)), // 保留3位小数 relevance: this.getRelevanceLabel(r.score) // 'high'|'medium'|'low' })); } }

5.3 生产环境配置:config.ts

// src/config.ts export interface MemoryConfig { host: string; port: number; // 坑1:永远不要在配置里写密码!从环境变量读取 password?: string; // 坑2:TTL必须可配置,不同环境差异巨大 ttl: { preference: number; // 秒 history: number; feedback: number; }; // 坑3:BM25参数必须可调,上线后要根据bad case优化 bm25: { k1: number; // 通常1.5-2.0 b: number; // 通常0.75 min_score: number; // 过滤低质量结果 }; } // 坑4:配置必须有默认值,防止环境变量缺失 export const defaultConfig: MemoryConfig = { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379'), password: process.env.REDIS_PASSWORD, ttl: { preference: 2592000, // 30天 history: 604800, // 7天 feedback: 86400 // 1天 }, bm25: { k1: 1.8, b: 0.75, min_score: 0.15 } };

5.4 前端集成示例:useAgentMemory.ts

// src/hooks/useAgentMemory.ts import { useState, useEffect } from 'react'; import { AgentMemory } from '../memory'; // 坑1:不要在组件内创建memory实例!会造成内存泄漏 const memory = new AgentMemory(defaultConfig); export function useAgentMemory() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); // 坑2:recallStream必须用for await,不能用map const recall = async (query: string) => { setIsLoading(true); setError(null); try { const results = []; // 正确:流式消费,内存友好 for await (const item of memory.recallStream(query, { limit: 5 })) { results.push(item); } return results; } catch (e) { setError(e instanceof Error ? e.message : '记忆检索失败'); return []; } finally { setIsLoading(false); } }; return { recall, isLoading, error }; } // 坑3:在useEffect里调用recall时,必须处理组件卸载 export function useUserPreferences(userId: string) { const [prefs, setPrefs] = useState<any>(null); useEffect(() => { let isMounted = true; const loadPrefs = async () => { const result = await memory.recall({ user_id: userId, query: "用户界面偏好" }); if (isMounted) setPrefs(result[0]); }; loadPrefs(); return () => { isMounted = false }; // 清理函数 }, [userId]); return prefs; }

5.5 线上避坑清单:那些让我加班到凌晨的10个教训

序号问题现象根本原因解决方案影响等级
1Agent响应延迟突增至2秒Redis连接池耗尽,新请求排队改用分级连接池,核心路径独占20连接⚠️⚠️⚠️⚠️⚠️
2用户偏好偶尔丢失expire命令在hSet前执行,key未创建EXPIRE替代SETEX,确保key存在再设TTL⚠️⚠️⚠️⚠️
3中文检索返回乱码Node.js进程编码非UTF-8启动脚本加export NODE_OPTIONS="--icu-data-dir=/path"⚠️⚠️⚠️
4Docker部署后内存占用翻倍Redis未配置maxmemoryredis.confmaxmemory 2gb+maxmemory-policy allkeys-lfu⚠️⚠️⚠️⚠️⚠️
5BM25检索结果顺序随机Wasm模块未正确初始化init()调用加await,且全局单例⚠️⚠️⚠️
6多个Agent实例写入冲突未用Redis事务MULTI/EXEC包裹hSet+expire操作⚠️⚠️⚠️⚠️
7前端报错“Cannot find module”WASM文件路径错误Webpack配置resolve.fallback指向正确目录⚠️⚠️
8用户注销后记忆未清除未监听注销事件在Auth SDK中注入onLogout钩子调用memory.clear()⚠️⚠️⚠️⚠️⚠️
9K8s滚动更新时连接拒绝哨兵未配置down-after-milliseconds设为5000ms,低于K8s readiness探针间隔⚠️⚠️⚠️⚠️
10日志里大量“Connection refused”Redis密码含特殊字符未转义密码URL编码:encodeURIComponent(password)⚠️⚠️⚠️

最后一个血泪教训:永远在CI/CD流水线里加入Redis兼容性测试。我曾因Redis 7.0的ACL变更,导致生产环境AUTH命令失败。现在我们的测试脚本会:

# 测试Redis基础能力 redis-cli -h $REDIS_HOST PING && echo "✓ Ping OK" redis-cli -h $REDIS_HOST SET test "ok" && echo "✓ SET OK" redis-cli -h $REDIS_HOST GET test | grep "ok" && echo "✓ GET OK"

6. 前端人的Agent进阶路径:从记忆模块到自主智能体的跨越

写完这个记忆模块,我重新审视了“前端工程师”的定义。过去我们说“懂HTML/CSS/JS就能上岗”,现在必须加上“理解状态在分布式系统中的生命周期”。这个模块不是终点,而是前端能力边界的破壁锤——它让我看清了三条清晰的进阶路径,每一条都扎根于前端最擅长的领域,却又通向Agent开发的核心腹地。

6.1 路径一:从记忆模块到决策引擎——把React状态机升级为Agent工作流

记忆模块解决了“记住什么”,但没解决“何时调用记忆”。我正在将React的useReducer模式,映射到Agent的状态机驱动工作流中:

  • initialState→ Agent的初始意图(如“帮用户订机票”)
  • reducer→ 一组可组合的Action(FETCH_USER_PREFERENCES,QUERY_FLIGHTS,CONFIRM_BOOKING
  • dispatch→ 根据记忆模块返回的上下文,动态选择下一步Action

关键突破是用TypeScript类型系统约束Agent行为

type FlightBookingState = { status: 'idle' | 'searching' | 'selecting' | 'confirming'; preferences: UserPreference; flights: Flight[]; }; type FlightBookingAction = | { type: 'SEARCH_START'; origin: string; dest: string } | { type: 'FLIGHT_SELECTED'; flightId: string } | { type: 'BOOK_CONFIRMED'; bookingRef: string }; // 编译时就能捕获:不能在'searching'状态下dispatch 'BOOK_CONFIRMED' function flightReducer(state: FlightBookingState, action: FlightBookingAction) { switch (state.status) { case 'idle': if (action.type === 'SEARCH_START') return { ...state, status: 'searching' }; break; } }

这不再是“写页面”,而是用前端最熟悉的范式,构建可验证、可调试、可协作的Agent逻辑

6.2 路径二:从UI组件到Agent界面协议——让设计师也能参与智能体设计

设计师总抱怨:“AI的反馈太机械,不像真人。”问题不在模型,而在界面与Agent的通信协议太原始。我正推动一个“Agent UI Protocol”标准:

  • ><!-- 设计师交付的代码 --> <button >
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 4:04:47

大专以下转行嵌入式?这行真正卡人的不是学历,而是这三样

网上有句话流传挺广&#xff1a;“张雪峰来了都不建议大专以下转行嵌入式。”我第一次看到这句话的时候&#xff0c;正在给一块STM32板子调串口&#xff0c;屏幕上一片乱码&#xff0c;说实话那一瞬间有点想笑&#xff0c;也有点被刺痛。但冷静下来想想&#xff0c;这句话能火&…

作者头像 李华
网站建设 2026/9/10 4:03:46

TVBoxOSC 安装教程:3 步让闲置电视盒子变免费媒体中心

TVBoxOSC 安装教程&#xff1a;3 步让闲置电视盒子变免费媒体中心 【免费下载链接】TVBoxOSC TVBoxOSC - 一个基于第三方项目的代码库&#xff0c;用于电视盒子的控制和管理。 项目地址: https://gitcode.com/GitHub_Trending/tv/TVBoxOSC 抽屉里吃灰的旧电视盒子&#…

作者头像 李华
网站建设 2026/9/10 4:01:32

Fanuc FOCAS二次开发:C#调用DLL对接数控机床

简介&#xff1a;本资源是面向工业自动化领域开发者与数控系统集成工程师的Fanuc数控机床二次开发核心工具包&#xff0c;聚焦Focas协议通信与底层API调用&#xff0c;解决设备数据采集、远程监控及定制化HMI开发等实际工程问题。压缩包为ZIP格式&#xff0c;大小26.05MB&#…

作者头像 李华