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

Agent设计:纯推理句柄与状态分离

前言

agent 组件是 nexus agent 核心层的枢纽——所有能力在这里装配,所有执行形态从这里发起。这篇讲它的形状为什么是这样。

Agent——nexus 里的纯推理句柄。一句话定位:只持能力,不持状态。能力是 llm、tools、skills、interceptors、system prompt、maxSteps、mode;状态(会话历史、压缩、memory)一个都不在它身上,全在 AgentSession。

这条边界是整个 nexus 最重要的一条。Agent 可以被任意多个 AgentSession 复用—— 100 万用户的客服系统,按角色建几个 Agent(账单、技术、退款),每个用户一个 Session,共享同一套 Agent 配置和模型 client。如果状态长在 Agent 上,每个用户都得 new 一个,模型 client 无法池化,配置无法共享。

核心设计

本质上agent是一个粘合层,负责把各种能力结合起来转发给LLM。agent的核心模型

    // Agent 是纯推理句柄:只持能力(llm/tools/skills/interceptors/system/maxSteps),// 不持会话状态。一个 Agent 可跨多个 AgentSession 复用。// Run/Resume 走 reAct loop:无 tools 时第一轮即退出(等价 direct 单次调用),// 有 tools 时多轮 tool 调用直至 stop。plan-execute 范式用 PlanExecAgent。type Agent struct {name stringdesc stringSystem stringllm llm.LLMClientskills *Skillstools []tools.TooltoolMap map[string]tools.TooltoolInterceptor []tools.ToolInterceptorllmInterceptor []llm.LLMInterceptorhooks *hooks.HooksmaxSteps intmaxPlanSteps intmode AgentMode}

    从核心模型能看到agent基本只有几个核心概念的组装外加tool和llm拦截器组成。

    初始化支持两种方式

      // NewAgent 轻量入口func NewAgent(name, system string, l llm.LLMClient, opts ...Option) *Agent {cfg := &AgentConfig{Name: name, System: system, LLM: l}for _, o := range opts {o(cfg)}return NewAgentWithConfig(cfg)}// NewAgentWithConfig 重度/配置驱动入口:直接传 AgentConfig。// 适合参数多、或从序列化配置加载的场景。func NewAgentWithConfig(cfg *AgentConfig) *Agent {cfg.apply()a := &Agent{name: cfg.Name,desc: cfg.Desc,System: cfg.System,llm: cfg.LLM,tools: cfg.Tools,toolMap: make(map[string]tools.Tool, len(cfg.Tools)),skills: cfg.Skills,hooks: cfg.Hooks,toolInterceptor: cfg.ToolInterceptors,llmInterceptor: cfg.LLMInterceptors,maxSteps: cfg.MaxSteps,maxPlanSteps: cfg.PlanMaxSteps,mode: cfg.Mode,}if a.skills != nil {a.System = a.skills.Prompt(a.System)a.tools = append(a.tools, a.skills.skillTools()...)}for _, t := range cfg.Tools {a.toolMap[t.Schema().Name] = t}if a.hooks != nil {a.toolInterceptor = append(a.toolInterceptor, a.hooks.HookInterceptor)}return a}

      NewAgent比较轻量,通过option的方式,而NewAgentWithConfig则是直接通过配置构造

      这里值得额外说一句的是可以看到skill是如何构造并且注入到agent和llm的,通过agent的system prompt加上自己的skill 重写了原来的system prompt(也许不重写只是在run的时候构造更合理),然后把skill 转化成tool给llm调用

      重写逻辑

        func SkillSystemPrompt(prompt string, ss ...*Skill) string {if len(ss) == 0 {return prompt}var b strings.Builderb.WriteString("Available skills — if one seems relevant to the task, " +"call read_skill(name) to load its full instructions before proceeding:\n")for _, s := range ss {b.WriteString(s.Summary()) // "- name: desc\n"}return b.String()}

        这是默认逻辑,业务也可以自己替换

          type SkillOptions struct {systemPrompt skills.SystemPromptFuncloader skills.Loader}func (o *SkillOptions) apply() {if o.systemPrompt == nil {o.systemPrompt = skills.SkillSystemPrompt}if o.loader == nil {o.loader = skills.SkillLoader(skills.DirLoader)}}type SkillOption func(opts *SkillOptions)func WithSystemPrompt(systemPrompt skills.SystemPromptFunc) SkillOption {return func(opts *SkillOptions) { opts.systemPrompt = systemPrompt }}func WithLoader(loader skills.Loader) SkillOption {return func(opts *SkillOptions) { opts.loader = loader }}// Skills 支持skill注入agent。构造时加载并过滤掉依赖未满足的 skill;// Prompt 把 skill 摘要(含 read_skill 指令头)拼进 system prompt;// skillTools 暴露 read_skill / read_skill_resource 两个工具,供 LLM 按需加载 skill 全文与资源。type Skills struct {systemPrompt skills.SystemPromptFuncloader skills.Loaderskills []*skills.Skill}

          还有就是通过拦截器把hook注入到每一次tool的调用进行拦截,这样我们就可以很轻松实现比如pause,审批,human in the loop的能力。

            func (h *Hooks) HookInterceptor(next tools.ToolHandler) tools.ToolHandler {return func(ctx context.Context, params map[string]interface{}, s tools.Schema) (string, error) {for _, hook := range h.rules {if !hook.Matcher.Match(s, params) {continue}switch hook.Action {case ActionDeny:return hook.Reason, nilcase ActionAsk:// 命中规则 执行审批动作decision, err := h.fn(ctx, hook, params, s)if err != nil {return "", err}if decision.Action == ActionDeny {return decision.Feedback, nil}default:}}return next(ctx, params, s)}}

            agent 需要实现核心接口

              // Agents agent 对外接口type Agents interface {Name() stringDescription() string// Run 单次任务执行Run(ctx context.Context, req *AgentRequest) (resp *llm.LLMResponse, err error)// Resume 对轮对话,长任务,有状态执行Resume(ctx context.Context, sess *AgentSession, req *AgentRequest) (*llm.LLMResponse, error)}// AgentsStream 流式执行能力,按需实现。*Agent(ReAct/PlanExec)实现,// TransferAgent/Workflow 后续按需补。type AgentsStream interface {AgentsRunStream(ctx context.Context, req *AgentRequest) (<-chan AgentEvent, error)//ResumeStream(ctx context.Context, sess *AgentSession, req *AgentRequest) (<-chan AgentEvent, error)}

              这里拆分了两个接口,一个是chat模式接口,一个支持stream模式接口,

              Run 是无状态单发:每次现拼 system + user 两条消息,跑完即弃。无 tools 时第一轮就退出(等价一次普通 Chat);有 tools 时多轮 tool 调用到 stop。这是"问一句答一句"的场景。

              RunStream 是流式版:run() 在后台 goroutine 跑,通过 ch chan<- AgentEvent 推事件(AgentText/Reason/ToolCall/ToolResult/Final/Paused)。ch 是否为 nil 是区分流式/非流式的开关——同一个 run(),两种取数。注意 defer close(event) 和 ErrPaused 透传:暂停不是错误,不推 Err 事件。

              Resume 是有状态多轮:核心是"Agent 纯推理,不管理 session 生命周期"。Load(rehydrate)由调用方在 Resume 前调,Agent 不操心;Agent 只负责把 sess.Messages() 当历史拼进 messages、跑完把新消息 sess.Append 持久化。

              具体实现

                func (a *Agent) Name() string {return a.name}func (a *Agent) Description() string {return a.desc}func (a *Agent) RunStream(ctx context.Context, req *AgentRequest) (<-chan AgentEvent, error) {event := make(chan AgentEvent, 16)go func() {defer close(event)// ReAct 模式事件已在 loop 里推(AgentText/Reason/ToolCall/ToolResult/Final/Paused);// 这里只兜底非 paused 的错误。resp 无需再推(AgentFinal 已在 ReAct 里推)。if _, err := a.run(ctx, req, event); err != nil && !errors.Is(err, ErrPaused) {sendEvent(ctx, event, AgentEvent{Err: err})}}()return event, nil}// Run 无状态单发,走 reAct loop。无 tools 时第一轮即退出(等价 direct 单次调用)。func (a *Agent) Run(ctx context.Context, req *AgentRequest) (resp *llm.LLMResponse, err error) {return a.run(ctx, req, nil)}

                这里可以看到不管是stream还是chat我们核心是统一的,所以run可以复用起来。

                  func (a *Agent) run(ctx context.Context, req *AgentRequest, ch chan<- AgentEvent) (resp *llm.LLMResponse, err error) {messages := []llm.Message{{Role: "system", Content: a.System},{Role: "user", Content: req.Input},}if req.Mode == 0 {req.Mode = a.mode}switch req.Mode {case ReAct:resp, _, err = a.ReAct(ch).Run(ctx, req, messages)case PlanExec:resp, _, err = a.Plan(ch).Run(ctx, req, messages)}return resp, err}

                  ReAct() 和 Plan() 是工厂方法,返回 ReActAgent / PlanExecAgent——真正的循环执行器。Agent 本身不实现循环,它只装配能力、分发到执行器。这样新增范式(比如 Reflexion、Tree of Thought)只需加一个执行器 + 一个 Mode 常量,Agent 主体不动。循环是策略,装配是机制,又一条边界。

                  Resume 有状态调用,相比Run,有状态需要记录每一次过程,所以引入agent session进行独立存储,压缩等处理。sess每次都会把ReAct 产生的new messages存储到内部,方便统一处理,在框架上很干净。

                    func (a *Agent) Resume(ctx context.Context, sess *AgentSession, req *AgentRequest) (*llm.LLMResponse, error) {if sess == nil {return nil, ErrNoSession}history := sess.Messages()messages := make([]llm.Message, 0, len(history)+2)messages = append(messages, llm.Message{Role: "system", Content: a.System})messages = append(messages, history...)messages = append(messages, llm.Message{Role: "user", Content: req.Input})// 持久化 usersess.Append(ctx, llm.Message{Role: "user", Content: req.Input})resp, newMsgs, err := a.ReAct(nil).Run(ctx, req, messages)if err != nil {if errors.Is(err, ErrPaused) {// 暂停:落盘已产生消息,透传 ErrPaused 供调用方识别;续跑时 session 历史完整sess.Append(ctx, newMsgs...)return nil, err}return nil, err}// 持久化 新产生的消息sess.Append(ctx, newMsgs...)return resp, nil}

                    最终都会转移到一个emit封装,这个helper函数帮助agent屏蔽底层llm的chat和stream接口差异

                      // emit 单轮 LLM 调用:ch==nil 走非流式 Chat(过 llmInterceptor),ch!=nil 走 Stream(转发 delta)。// 两种模式都返回完整 *LLMResponse(StreamFinal 或 Chat),交给 ReAct 按 FinishReason 决定下一步。// 注意:stream 路径暂未走 llmInterceptor,待 StreamInterceptor 落地后补。func (a *Agent) emit(ctx context.Context, msgs []llm.Message, schemas []tools.Schema, ch chan<- AgentEvent) (*llm.LLMResponse, error) {if ch == nil {return llm.Chain(a.llmInterceptor...)(a.llm.Chat)(ctx, msgs, schemas)}stream, err := a.llm.Stream(ctx, msgs, schemas)if err != nil {return nil, err}for {select {case <-ctx.Done():return nil, ctx.Err()case chunk, ok := <-stream:if !ok {return nil, fmt.Errorf("stream closed without final")}if chunk.Err != nil {return nil, chunk.Err}switch chunk.Type {case llm.StreamText, llm.StreamReason:// 转发 delta 给上层展示;StreamTool 累积态不转发(ReAct 执行前推 AgentToolCall,避免重复)sendEvent(ctx, ch, AgentEvent{Type: AgentEventTypeFromLLM(chunk.Type),Response: chunk.Response,})case llm.StreamFinal:// 单轮结束,不推事件——交 ReAct 按 FinishReason 决定 AgentToolCall/AgentFinalreturn chunk.Response, nil}}}}

                      最后总结

                      设计可以压缩成一句:Agent 是纯推理句柄——构造时接线(skills/hooks 自动注入),运行时分发(ReAct/Plan 工厂),执行形态三选一(Run/RunStream/Resume),流式非流式同构(emit)。所有"状态"都被推到 AgentSession,所有"策略"都被推到执行器和 hooks,agent.go 自己只保留"装配 + 分发"这一层最薄的机制。这层薄机制,是 nexus 整个 agent 体系的支点。

                      http://www.jsqmd.com/news/1369608/

                      相关文章:

                    • 青岛窗户漏风漏水去哪里修?本地门窗直营店德朗驰,上门换玻璃换胶条 - Gsydold
                    • TBR 瓦片式渲染解析:移动 GPU 的带宽与功耗架构优化
                    • 电磁学三大定律解析与电力电子工程应用
                    • 基于Foreign Table加速查询MaxCompute数据
                    • 后缀树:原理、构建与应用详解
                    • MSTP实验2: MSTP配置
                    • 15天学会AI应用开发(十七)使用LangGraph实现会话记忆功能
                    • Win11Debloat:Windows系统精简与性能优化终极指南
                    • 靠谱降AI率工具怎么选?靠谱工具选择全指南
                    • 摄影器材海外红人营销:从头部KOL到微型红人矩阵的转型
                    • Cat6A网线替代方案:技术选型与性能验证
                    • LangChain Agent实战:从工具调用到智能体架构的工程实现
                    • Kimi K3开源大模型实测:从本地部署到应用场景全解析
                    • 茱萸绛囊(重阳茱萸绛囊),河南上蔡省级非物质文化遗产
                    • 鸣潮智能自动化助手ok-ww:基于图像识别的开源游戏辅助方案
                    • Python Flask + ECharts 构建实时比赛排名可视化系统
                    • BiliTools的AI总结功能如何帮你从B站视频中提取知识精华?
                    • Mac Mouse Fix:解锁你鼠标的隐藏潜能,让普通鼠标在macOS上超越触控板
                    • 从Claude Code源码看Multi-Agent系统:任务分发与团队协作的工程实践
                    • SpringBoot+Vue3在线教育系统开发实战
                    • 如何快速批量导出飞书文档:面向企业的完整解决方案
                    • AI时代理发师的未来:人机协作下的职业进化与价值重塑
                    • 如何在5分钟内用Whisky让Apple Silicon Mac运行Windows应用:终极兼容解决方案
                    • Unity透明视频播放全攻略:AVPro Video配置与Alpha通道处理
                    • 品牌实体图谱建设:从零构建AI可识别的品牌知识体系
                    • SAP S/4HANA部署与实施全解析:从战略选择到工程落地
                    • VC2022下xlnt库编译配置与Excel读写实战指南
                    • 5分钟彻底告别网盘限速:九大平台直链下载助手实战指南
                    • Python音频批量处理工具:基于FFmpeg的图形化切割与格式转换方案
                    • Excalidraw 文件格式(白板画图)-Day14