当前位置: 首页 > article >正文

ReAct 循环的 50 行 Go 实现,逐行拆解

ReAct 循环的 50 行 Go 实现逐行拆解系列「企业级 AI Agent 实现拆解」第三篇。上一篇讲了 Session 聚合根和状态机——状态怎么迁移、事件怎么发、终态怎么判。但状态机本身是静态的谁在驱动这些迁移答案是RunTurnHandler.Handle()——每一轮用户消息进来它负责加载会话、组装上下文、把 ReAct 循环跑起来、然后落库收尾。先说一个架构决策ReAct 循环本身LLM 推理 → 工具调用 → 观察 → 继续推理我们没自己写交给 Eino 的react.NewAgent()跑。Handle()只做框架解决不了的部分Session 状态迁移、多租户 context 注入、长期记忆召回、事务落库、异步记忆提炼。下面是去掉错误处理样板代码后的主干大约 50 行func(h*RunTurnHandler)Handle(ctx context.Context,in RunTurnInput)error{// ① 加载会话校验状态sess,_:h.repo.Load(ctx,in.SessionID)ifsess.IsTerminal(){returnerrors.New(session already terminal)}// ② context 注入ctxcontext.WithValue(ctx,port.ContextKeyAgentTenantID{},sess.TenantID())ctxcontext.WithValue(ctx,port.ContextKeyAgentUserID{},sess.UserID())ctxcontext.WithValue(ctx,port.ContextKeyAgentSessionID{},string(sess.ID()))// ③ 前置 hook 加载配置h.invokeHook(func(hr port.HookRunner){hr.BeforeSession(ctx,sess.ID())})cfg,_:h.configs.Load(ctx,sess.TenantID(),sess.AgentConfig())// ④ 判断路径新 turn 还是 HITL 恢复isResume:in.UserTextrunInput:port.RunStreamInput{IsResume:isResume}varcapturedHistory[]model.MessageifisResume{// ⑤a HITL 恢复状态迁移 WAITING→RUNNING先落库再跑sess.Resume(*in.Decision)h.persist(ctx,sess)}else{// ⑤b 新 turn拉历史、写 user 消息、召回记忆、拼 system prompthistory,_:h.repo.ListMessages(ctx,sess.ID())sess.IncTurn()userMsg:model.NewUserMessage(sess.ID(),sess.TurnCount(),in.UserText)h.repo.AppendMessage(ctx,userMsg)historyappend(history,userMsg)capturedHistoryhistory memCtx:h.recallMemory(ctx,sess.TenantID(),sess.UserID(),in.UserText)runInput.SystemContentexpandSystemPrompt(cfg.SystemPrompt,sess)memCtx runInput.Historyhistory}// ⑥ 交给 EinoReAct 循环、工具调用、Hook callback 全在里面tr,interrupt,err:h.runnableFac.StreamTurn(ctx,cfg,sess.TenantID(),runInput,h.hooks,h.stream,sess.ID(),)iferr!nil{returnh.failSession(ctx,sess,...)}// ⑦ 处理 HITL 中断ifinterrupt!nil{returnh.handleEinoInterrupt(ctx,sess,interrupt)}defertr.Close()// ⑧ 消费流token → SSE turn.delta 收集 finalContentfinalContent,_:h.consumeEinoStream(ctx,sess.ID(),tr)// ⑨ 落库 触发终态事件 异步记忆提炼asstMsg:model.NewAssistantMessage(sess.ID(),sess.TurnCount(),finalContent,nil)sess.Complete()h.commitFinalAnswer(ctx,sess,asstMsg)ifh.memory!nil!isResumelen(capturedHistory)0{h.scheduleMemoryExtraction(sess,capturedHistory,finalContent)}returnnil}接下来逐块说每一段在做什么以及为什么这样设计。① 加载会话校验状态sess,err:h.repo.Load(ctx,in.SessionID)iferr!nil{returnh.onError(ctx,in.SessionID,err)}ifsess.IsTerminal(){returnerrors.New(session already terminal)}终态COMPLETED / FAILED / CANCELLED的 Session 不可以再跑新 Turn这是状态机的硬约束。在入口做这个检查比在循环里做更干净。② context 注入为什么用context.WithValue而不是 struct 字段因为 Runnable 按 AgentConfig 维度缓存跨 session 复用——如果 sessionID 绑到 struct 字段上缓存完第一个 session 后就错了。从 context 动态读缓存才能安全共享。ctxcontext.WithValue(ctx,port.ContextKeyAgentTenantID{},sess.TenantID())ctxcontext.WithValue(ctx,port.ContextKeyAgentUserID{},sess.UserID())ctxcontext.WithValue(ctx,port.ContextKeyAgentSessionID{},string(sess.ID()))这三个值随 context 一路传进 Eino 的chatModelAdapter和toolBrokerAdapter后者从 context 取出来做租户隔离和工具路由。③ 前置 hook 加载配置h.invokeHook(func(hr port.HookRunner){_hr.BeforeSession(ctx,sess.ID())})cfg,err:h.configs.Load(ctx,sess.TenantID(),sess.AgentConfig())BeforeSession是 Hook 链的第一个阶段典型用途是认证验证、配额预检、tracing 初始化。invokeHook做了 nil 检查没有注入 HookRunner 时静默跳过。AgentConfig包含 system prompt 模板、LLM profile、工具白名单、MaxTurns、HITL 开关等每次 Turn 都重新加载配置变更即时生效。④ 判断路径isResume:in.UserText一个布尔值区分两条路径UserText 表示 HITL 恢复从 Eino checkpoint 继续否则是新 Turn。两条路径的准备工作不同但最终都走同一个StreamTurn调用。⑤a HITL 恢复路径ifsess.State()!model.StateWaiting{returnfmt.Errorf(resume called but session not waiting, state%s,sess.State())}dec:model.InterruptDecision{Action:model.DecisionApprove}ifin.Decision!nil{dec*in.Decision}iferr:sess.Resume(dec);err!nil{_h.onError(ctx,sess.ID(),err)returnerr}iferr:h.persist(ctx,sess);err!nil{returnerr}先落库再跑。sess.Resume(dec)把状态从 WAITING 迁移到 RUNNING立刻写库。这样进程崩溃重启时session 已经是 RUNNING重试逻辑可以正确处理。如果先跑再落库崩溃后 session 还是 WAITING重试会重新走恢复路径可能重复执行。isResumetrue时runInput.History和runInput.SystemContent保持零值——Eino 收到nilinputMsgs 时自动从 CheckpointStore 加载上次暂停时的图状态。⑤b 新 turn 路径history,_:h.repo.ListMessages(ctx,sess.ID())sess.IncTurn()userMsg:model.NewUserMessage(sess.ID(),sess.TurnCount(),in.UserText)h.repo.AppendMessage(ctx,userMsg)historyappend(history,userMsg)capturedHistoryhistory memCtx:h.recallMemory(ctx,sess.TenantID(),sess.UserID(),in.UserText)runInput.SystemContentexpandSystemPrompt(cfg.SystemPrompt,sess)memCtx runInput.Historyhistory五件事按顺序做拉历史 → 增轮次计数 → 写 user 消息 → 召回长期记忆 → 组装 system prompt。recallMemory用本轮用户文本检索相关记忆片段追加到 system prompt 末尾失败时静默返回空字符串。expandSystemPrompt展开{{user_id}}、{{tenant_id}}、{{time}}三个占位符。capturedHistory留着会话结束后给记忆提炼用。⑥ 交给 Einotr,interrupt,err:h.runnableFac.StreamTurn(ctx,cfg,sess.TenantID(),runInput,h.hooks,h.stream,sess.ID(),)这一行是整个Handle的边界线左边是 DDD 领域逻辑右边是 Eino 框架内部。为什么切在这里而不是更早比如让 Eino 接管整个 Handle或更晚手写 ReAct for 循环因为状态加载、context 注入、记忆召回这些事和 LLM 编排无关框架不该管而 LLM 调用、工具分发、流式输出这些事已经有成熟实现没必要重写。这条边界让两边各自演进——换 Eino 版本不影响状态机改记忆策略不影响 Runnable 缓存。StreamTurn内部查 Runnable 缓存TTL 35 分钟→ 未命中则react.NewAgent()构建并编译 → 设置 compose 选项checkpoint ID、callbacks、force-new-run→runnable.Stream()→ 检测 interrupt 信号。应用层完全不感知这些。返回三种情况含义清晰返回值含义(TokenReader, nil, nil)正常流消费到 Final Answer(nil, *AgentInterruptInfo, nil)Eino 图级 HITL 中断(nil, nil, err)执行错误⑦ 处理 HITL 中断ifinterrupt!nil{it:model.NewPreToolInterrupt(hitl: tool call requires approval,model.ToolCall{},h.interruptTTL)sess.Pause(it)h.stream.Emit(ctx,sess.ID(),port.StreamEvent{Type:interrupt,Payload:map[string]any{before_nodes:info.BeforeNodes},})returnh.persist(ctx,sess)}中断信号来自 Eino 图级的WithInterruptBeforeNodes([tools])不是 Hook 返回值。Hook 的BeforeToolUse在 Eino 的 Callback 层执行应用层看到的是 Eino 已经决定了要中断只需要把 Session 状态迁移到 WAITING 并推 SSE 通知前端。⑧ 消费流varsb strings.Builderfor{content,err:tr.Recv()iferrors.Is(err,io.EOF){break}iferr!nil{returnsb.String(),err}ifcontent!{sb.WriteString(content)h.stream.Emit(ctx,sid,port.StreamEvent{Type:turn.delta,Payload:content})}}port.TokenReader是框架无关接口Recv() (string, error)。Eino 的schema.StreamReader在einoadapter包里被包装成这个接口这里看不到任何 Eino 类型。LLM 吐一个 tokenSSE 就推一帧没有额外 goroutine没有缓冲区。⑨ 落库、触发终态、异步记忆提炼asstMsg:model.NewAssistantMessage(sess.ID(),sess.TurnCount(),finalContent,nil)sess.Complete()h.commitFinalAnswer(ctx,sess,asstMsg)ifh.memory!nil!isResumelen(capturedHistory)0{h.scheduleMemoryExtraction(sess,capturedHistory,finalContent)}三件事commitFinalAnswer优先走事务路径消息写入和 session 状态更新在同一个数据库事务里。写完后emitTerminalEvents触发AfterSessionhook 和 SSEdone帧。注意persist()和emitTerminalEvents()是两个独立函数——persist只写库发事件SSEdone由emitTerminalEvents单独控制避免事务和非事务两条路径重复触发。scheduleMemoryExtraction起一个 goroutine30 秒超时把本轮 user/assistant 消息异步发给 memory BC 提炼记忆。失败只触发OnErrorhook不阻塞响应返回。HITL 恢复路径isResumetrue跳过避免同一会话重复提炼。流程图Handle(in) │ ├─ Load IsTerminal 检查 ├─ context 注入tenantID/userID/sessionID ├─ BeforeSession hook Load AgentConfig │ ├─── isResumetrue ──────────────────────────────────────┐ │ StateWaiting 校验 │ │ sess.Resume → persist先落库 │ │ runInput: IsResumetrue, Historynil │ │ │ └─── isResumefalse ─────────────────────────────────────┤ ListMessages IncTurn AppendMessage │ recallMemory expandSystemPrompt │ runInput: SystemContent History │ ↓ runnableFac.StreamTurn(...) ┌── Eino 内部 ──────────────┐ │ react.NewAgent ReAct 循环│ │ 工具调用 Hook callback │ └──────────────────────────┘ │ ┌─────────────┼──────────────┐ interrupt TokenReader error │ │ │ sess.Pause() consumeEinoStream failSession SSE interrupt SSE turn.delta persist 收集 finalContent │ sess.Complete() commitFinalAnswer事务 emitTerminalEvents → SSE done scheduleMemoryExtractiongoroutine小结回头看这 50 行主干Handle()的核心职责不是跑 ReAct 循环——那件事 Eino 做了。它的真正工作是做好框架不管的事Session 状态迁移的原子性、多租户 context 的透传、记忆的召回和异步提炼、事务落库和终态事件解耦。这些事情没有框架能替你做因为它们和具体业务域强绑定。改 Eino 版本不影响状态机改记忆策略不影响 Runnable 缓存改 Hook 不影响事务落库——这条边界线的价值就在这里。下一篇看 HITL 完整路径从 Eino 图级中断到人工审批再到 checkpoint 恢复执行。下一篇HITL 中断 —— 让人类随时叫停 AI 的正确姿势

相关文章:

ReAct 循环的 50 行 Go 实现,逐行拆解

ReAct 循环的 50 行 Go 实现,逐行拆解 系列「企业级 AI Agent 实现拆解」第三篇。上一篇讲了 Session 聚合根和状态机——状态怎么迁移、事件怎么发、终态怎么判。但状态机本身是静态的,谁在驱动这些迁移? 答案是 RunTurnHandler.Handle()——…...

spring boot 11

一、分组校验(Spring Validation)1. 核心概念分组校验是 Spring Validation 提供的功能,用于在不同业务场景(新增 / 更新)下,对同一个实体类执行不同的校验规则,避免重复定义实体类。2. 分组校验…...

2026 年北京本土 GEO 优化服务商权威第三方测评

本文为 2026 年北京本土 GEO 优化服务商权威第三方测评内容,结合艾瑞咨询、IDC、Gartner 年度行业数据,筛选出北京地区口碑、技术、服务、效果综合实力前十的 GEO 优化公司。榜单核心首位为欧越曼GEO,凭借全域语义自研架构、北京总部本地化专…...

前端html字体包体积压缩,网站工程下字体压缩裁剪工具

整个网站项目如果字体包体积太大就会影响其加载速度,字体加载完会让页面字体突然变换。做一个工具他会自动检索网站上所有展现给用户的字符,然后原地裁剪字体。来解决这个问题。实现效果如下: 执行py文件以后,在网站字体文件所在目…...

AI Infra 后端开发工程师 — 学习路线

AI Infra 后端开发工程师 — 学习路线目标:3个月可切换到AI Infra后端岗位能力迁移地图AI Infra 对应迁移成本Goroutine调度/并发控制思维直接复用Agent会话生命周期管理设计模式相同SSE/WebSocket流式协议实时系统思维GPU显存管理/KV-cache资源约束优化思维推理延迟…...

厂二代接班创业和继承怎么选择

在家族企业传承的大背景下,厂二代面临着接班创业和继承家业的艰难抉择。据统计,民企二代接班成功率不足 30%,这凸显了传承过程中的挑战与风险。上海章动企业咨询有限公司作为企二代、厂二代接班传承管理咨询的可信渠道,在这方面有…...

2026 大模型企业画像梳理技术解析:混乱画像规范方法深度测评

引言随着 AI 搜索成为商业信息获取的主要渠道,大模型生成的企业画像准确性直接影响企业品牌形象和获客效果。据中国 GEO 行业协会 2026 年调研数据显示,超过 76% 的企业反映大模型生成的企业画像存在信息混乱、错误遗漏、业务不匹配等问题,其…...

别再重复造轮子了!这个开源论坛小程序(Java+Uniapp)一套代码搞定 App/小程序/H5/PC,私域流量神器

你是否有过这些想法? 我想做个类似“知识星球”的圈子小程序,但外包报价动辄 5 万起…… 公司要做私域社区,需要同时支持微信小程序和 App,难道要养两个开发团队? 想靠“付费帖子 会员 打赏”变现,去哪…...

1688代采系统开发避坑指南:经验之谈

做跨境代购系统三年了,技术栈换过一次。今天把当初的技术选型过程和踩坑经验整理出来。多页面架构(MPA)的选择:没有用 React/Vue SPA 做租户端前台的首页和商品页,而是传统的多页面(HTML JS jQuery&#…...

为什么越来越多公司坚持做背调?

很多中小企业都有一个误区:觉得背调“可有可无”、浪费时间、增加成本。但真实职场现状是:不做背调,才是企业最大的隐形成本。现在求职简历美化早已是常态,履历注水、项目造假、隐瞒纠纷、失信记录……肉眼面试根本看不出来。一次…...

用 TLA+ 形式化验证 Harness 的并发安全性

从零到一:用TLA+形式化验证Harness CI/CD平台的并发操作安全性 副标题:解决分布式环境下流水线执行、资源抢占、状态一致性的核心痛点 摘要/引言 如果你是云原生团队的开发或运维工程师,大概率遇到过这样的场景:两个生产部署流水线同时触发,同时抢占同一个K8s集群的环境…...

糖尿病风险预测系统:机器学习算法对比与区块链边缘计算架构实践

1. 项目概述与核心价值在慢性病管理领域,尤其是糖尿病防控,早期预警和精准风险评估是降低发病率和医疗负担的关键。传统的风险评估多依赖于医生经验和简单的问卷,难以处理多维度、非线性的复杂风险因素关联。近年来,以机器学习为代…...

C 语言自定义类型:结构体、位段、联合体、枚举

struct:成员并排,空间叠加位段:struct 里按 bit 分配,极致省空间union:成员重叠,空间共享enum:有限常量,替代宏,更安全一、结构体 struct:成员并排&#xff0…...

海思Hi3516CV608×PSRAM|AI全彩IPC黄金硬件方案

一、海思Hi3516CV608核心应用特性(AI全彩IPC主力主控)芯片原生内置512Mbit DDR2,满足系统运行、视频编码、基础ISP图像处理,硬件资源稳定可靠。集成硬件NPU(0.2TOPS),原生支持人形检测、越界侦测…...

大模型写论文,“文献”全是瞎编的?为什么大模型爱编参考文献?如何确保文献的真实性

一、引言:当AI开始“编文献” 随着大语言模型(如 GPT-4、DeepSeek、Claude 等)在学术写作、文献综述、技术调研中的普及,一个隐蔽但危险的问题逐渐暴露出来——文献幻觉(Citation Hallucination)。 你可能…...

拒绝盲从:从“上岸村”公考笔试机构推荐谈个性化备考路径

2026 年公考竞争持续升温,国考报名人数再创新高,考生群体日趋多元,需求正从 “有没有课上” 转向 “课程适配性与教学实效性”。行业正告别粗放式扩张,精细化深耕、价值化回归、场景化适配成为新的发展主线。在此背景下&#xff0…...

什么是vibe coding:概念解析与首选工具Trae实测

什么是vibe coding:概念解析与首选工具Trae实测你是否好奇vibe coding到底是什么,为何能成为2025年最火的开发方式?是否想知道vibe coding和传统编程的核心差异,以及用什么工具能高效落地?vibe coding是提示词驱动开发…...

工业IoT实战:边缘计算+AI在电机预测性维护中的系统架构设计

前言工业物联网(IIoT)场景下,预测性维护(Predictive Maintenance)是AI技术落地价值最明确的方向之一。本文以杭州沃伦森(WARENSEN)电气的AIESA电机智能安康系统为案例,分析其在边缘计…...

天勤 get_account 资金字段读懂:下单前可用与保证金检查

前言 策略信号对了却下不出去,我第一反应看 get_account():是 available 不够,还是把 balance 当可用去和保证金比了。有次模拟盘「明明没下单」却报资金不足,查了半天是字段读错;还有一次夜盘加仓,白天算好…...

金融数据宝藏库:沪深Level2与高频数据拆解

被高频数据搞懵了?硬盘空间就是这么没的。刚入坑那会儿,总想用最细的数据,结果光下载和整理就耗掉大半天,策略还没写呢。 今天简单聊聊几种常见的高频数据到底有啥区别,主要是沪深股票这块。数据来源是CMES金融数据库&…...

解锁 AI 新用法:2026 普通人办事效率翻倍实战指南

2026 年 5 月 22 日,国产 AI 大模型周调用量连续两周领跑全球,智能体(Agent)技术从概念落地为全民工具,AI 正从 “科技圈热词” 彻底变成普通人的 “效率外挂”。当下,文心一言 5.1、DeepSeek V4 等国产模型…...

从 @Tool 装饰器到 MCP,浅析大模型工具生态与 Function Calling 的底层逻辑

从 Tool 装饰器到 MCP,浅析大模型工具生态与 Function Calling 的底层逻辑 在开发 LLM Agent(大模型智能体)时,我们经常会遇到各种层出不穷的技术名词:Function Calling(函数调用)、JSON Schema…...

机器学习---监督学习入门实验全攻略(小白友好版)

新晋码农一枚,小编会定期整理一些写的比较好的代码和知识点,作为自己的学习笔记,试着做一下批注和补充,转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!…...

矩池云实战: 用Gemma 4 + Open WebUI打造你的私人OpenAI

在开源 AI 生态中,如何不依赖闭源 API,纯靠开源堆栈搭建出一套具备“深度思考(CoT)&原生多模态顶配开发环境? 答案是:Ollama Gemma-4-31B Open WebUI Ollama Gemma-4-31B Open WebUI 的真正核心价…...

架构测试方法体系:覆盖、验证与CHAM动态语义分析

一、引言:架构测试的三维框架 架构测试的独特挑战在于:它不仅要验证系统"做得对不对",更要验证"设计得对不对"。传统测试方法聚焦于代码层面的功能正确性,而架构测试关注的是结构合理性、组件交互正确性以及质量属性可达性。 根据测试目标的不同,架…...

2026年国内镜像站选择指南:一站接入GPT-5.5和主流AI模型

先交代一下背景。2026年的大模型格局已经变了——GPT-5.5、Claude、Gemini、Grok 各有各的强项,做项目的时候经常需要这个模型写代码、那个模型分析文档、再来一个做联网搜索。但问题是,国内想用上这些模型,光解决"能访问"就已经够…...

NotebookLM关键词提取结果不一致?权威测试报告揭示模型版本、文档编码、上下文窗口三重耦合陷阱

更多请点击: https://kaifayun.com 第一章:NotebookLM关键词提取 NotebookLM 是 Google 推出的基于用户文档构建的 AI 助手,其核心能力之一是自动从上传的文本中识别并提取关键语义单元——即关键词(Keywords)。这些关…...

AI智能体安全防御:从代码数据分离到多代理系统架构实践

1. 项目概述:当AI智能体成为攻击目标 最近和几个做AI应用落地的朋友聊天,大家不约而同地提到了同一个焦虑:模型能力越强,系统越复杂,心里反而越没底。一个能自主调用API、处理文件、联网搜索的智能体,一旦被…...

如何让抓取手机日志---ADB 从入门到实战:小米14日志抓包与连接详解

一、ADB 是什么? ADB 的全称是 Android Debug Bridge(安卓调试桥)。顾名思义,它就像一座桥梁,连接你的电脑和安卓手机。 Debug(调试):它的核心用途是帮助开发者调试应用、分析问题。…...

第一性原理缺陷计算准备:以氢掺杂氧化镓为例的VASP实践指南

1. 项目概述:从“掺杂”到“缺陷”的计算准备在半导体材料研究领域,尤其是宽禁带半导体,对材料进行掺杂以调控其电学、光学性质是核心课题之一。氧化镓(Ga2O3)作为一种新兴的超宽禁带半导体,因其在功率电子…...