Documentation
¶
Overview ¶
Package agentrun ReAct agent 运行样板:封装 eino ADK 的 ChatModelAgent + Runner 构造、事件流 drain、最终文本提取与失败重试。
从 Argus 的 R3 执行模式提炼——任何"单 agent 带工具自主循环"的场景 都可以直接用,不必重写 ADK 样板:
out, err := agentrun.Run(ctx, agentrun.Config{
Name: "reviewer",
Instruction: instruction,
Model: chatModel, // llm.Generator.RawModel()
Tools: tools, // eino 工具表(mcp/rag/skill...)
MaxIterations: 12,
}, query)
ReAct 出口判定:assistant 消息且不带 tool_calls 即最终答复; MaxIterations 耗尽仍未出口 → 报错(调用方可决定降级)。
Plan-and-Execute 编排样板(架构模式 3):封装 eino adk prebuilt/planexecute 的 三件套(Planner / Executor / Replanner)——Planner 把目标拆解为分步计划, Executor 带工具执行单步,Replanner 评估进度决定"完成任务"或"修订计划", "执行→重规划"循环直至收敛。
与 ReAct(agentrun.Run)的分工:ReAct 适合"边想边做"的短链路任务;P&E 适合 目标明确、步骤可预规划的长链路任务(计划先行,每步执行结果可审计)。 需要定制 Planner/Replanner 提示词或输入整形时,直接使用 eino planexecute 原语。
副作用感知的重试守卫:变更类(非幂等)工具一旦执行,整体重跑 = 重复副作用 (脚本执行/服务操作类工具在首轮已真机生效)。重试入口据此跳过整体重试, 宁可如实失败交上层降级/人工,不做「重跑一次赌成功」。
变更类判定是保守的动词段启发式:按下划线分段**精确**匹配动词表——子串匹配会把 use_skill(含 kill)、get_running_config(含 run)误判成变更类,白白放弃合法重试; 偏保守原则保留在词表侧——拿不准的执行动词都收进来。
Index ¶
Constants ¶
const ( EventReasoning = "reasoning" EventText = "text" EventToolCall = "tool_call" EventToolResult = "tool_result" EventReasoningDelta = "reasoning_delta" EventTextDelta = "text_delta" // EventAttemptRetry 内部整程重跑标记(v0.14.7):WithRetry 的首跑失败→重试边界 // 此前完全静默,时间线上只剩「两段近似重复环节」无从解释(bianque 批次二百九十五 // 为协议级重试补过同款留痕,内部级是词表缺口)。Text=首跑失败原因(含救回标注)。 EventAttemptRetry = "attempt_retry" )
事件类型词表。 注意与 acpx 包事件词表(text/tool_call/tool_result)互为平行词汇: 两侧有意保持同名字面量以便消费方对译;改动任一侧词表时同步检查另一侧。
流式语义(默认开流):reasoning_delta/text_delta 在生成过程中逐分片到达 (真流式),同轮收束后仍发完整 reasoning/text 快照(权威口径,回放/对账以它为 准;增量只服务实况渲染)。消费方以「同轮快照到达即吸收增量缓冲」去重—— 中间**调用轮**无 text 快照(终稿窗口才有),其 text_delta 缓冲以该轮的 tool_call 事件为吸收锚(调用轮前导正文不构成终稿;reasoning 逐轮有快照), 拼接口径=各轮窗口正文按序拼接、终稿窗口与 EventText 快照严格一致(第八轮 审计钉住的词表契约)。
const DefaultMaxIterations = 12
默认迭代上限(ADK ReAct 循环轮数;防失控循环)。
Variables ¶
var MutatingVerbs = map[string]bool{ "exec": true, "execute": true, "run": true, "restart": true, "kill": true, "reboot": true, "shutdown": true, "halt": true, "start": true, "stop": true, "install": true, "remove": true, "delete": true, "del": true, "create": true, "write": true, "set": true, "update": true, "apply": true, "deploy": true, "enable": true, "disable": true, "flush": true, "truncate": true, "mount": true, "umount": true, "unmount": true, "killall": true, "chmod": true, "chown": true, }
MutatingVerbs 变更类工具动词段表(可扩展:调用方按领域补充后 IsMutatingTool 即生效; 并发读取安全——构建期扩展、运行期只读)。
Functions ¶
func IsMutatingTool ¶
IsMutatingTool 工具名是否变更类(非幂等):小写后按下划线分段精确匹配动词表。
Types ¶
type Config ¶
type Config struct {
// Name agent 名(trace/日志标识)。
Name string
// Description 角色描述(进 agent 元信息)。
Description string
// Instruction 系统提示词(含方法论/工具策略等注入产物)。
Instruction string
// Model eino ChatModel(llm.Generator 的 RawModel())。
Model model.BaseChatModel
// Tools 工具表(建议经 toolprior.Table.Ordered(ctx) 产出)。
// 与 ToolsFactory 二选一;两者都设置时 ToolsFactory 优先。
Tools []tool.BaseTool
// ToolsFactory 工具表工厂:每次 run 调用新建一份工具表。
// WithRetry 场景建议设置——工具表含 toolprior.LimitCalls 等
// 有状态包装时,复用同一实例会让限流计数跨重试累计(重试继承 0 余额,
// 每次调用立即被拒)。工厂内每次重新包装即可让预算按尝试重置。
ToolsFactory func() []tool.BaseTool
// ToolAliases 工具**名别名**表(可选):键=工具的规范名(即工具表里
// `tool.Info().Name` 的值,MCP 场景为 `server.工具名`),值=别名配置。
// 模型用别名调用时会被解析回规范名,两种写法都命中。
//
// 为什么需要(实测 2026-10-30,扁鹊生产实弹 215):MCP 工具的暴露名带
// server 命名空间(`ask-ops.run_readonly_commands`),而模型常按直觉发
// **裸名**(`run_readonly_commands`)⇒ eino ToolsNode 直接报
// `tool ... not found in toolsNode indexes`,该步骤**整步失败**
// (事件错误向上抛、链路降级;变更类工具已执行时连整步重试都被跳过)。
// 下游把裸名注册成别名 ⇒ 不必依赖模型自我纠正。
//
// 注意:别名若与**另一个工具的规范名或已注册别名**冲突,eino 侧
// `applyAliasConfigs` 会让整个 ToolsNode 构造失败 ⇒ 装配期就要避免
// (同名裸名来自两个 server 时只能择一,或改用 UnknownToolsHandler 兜底)。
// nil/空=零行为(存量路径不变,eino 侧 aliasConfigs==nil 时跳过)。
ToolAliases map[string]compose.ToolAliasConfig
// UnknownToolsHandler **幻觉工具兜底**(可选):模型调用了工具表里
// 不存在的工具时,由本处理器接管而不是让 eino 报错。返回的字符串会
// 作为该次工具调用的结果回给模型,返回 error 则该步失败。
//
// 与 ToolAliases 的分工:别名解决**已知**的写法差异(首选——两种写法
// 都命中,模型不必自我纠正);兜底解决**未知**的幻觉(模型编了个工具名),
// 让它拿到一句可解释的观察而不是整步失败。
//
// 签名与 eino `compose.ToolsNodeConfig.UnknownToolsHandler` 一致:
// name=模型请求的工具名,input=该次调用的参数原文(JSON 字符串)。
// nil/空=零行为(存量路径不变:eino 侧未设时直接报错)。
UnknownToolsHandler func(ctx context.Context, name, input string) (string, error)
// MaxIterations ReAct 循环轮数上限(默认 12)。
MaxIterations int
// RetryAfterMutation 变更类工具已执行后仍允许整体重试(默认 false=守卫生效:
// 首轮调过变更类工具后失败不再重跑——重复副作用风险,如实上抛交调用方降级)。
// 只读/幂等工具场景可置 true 恢复无条件重试。
RetryAfterMutation bool
// NonMutatingTools 工具名豁免集(键=工具名,小写归一比对):表内工具即使命中
// 动词段启发式也不计变更类。消费方以 MCP ReadOnlyHint 注解/curated 白名单为据
// 填充——动词启发式对 run_readonly_command 类「run*」只读执行器命名误报,实弹
// (bianque 429 限流轮)首轮合法只读采集即挡死整轮重试,专家失败收场。
// 与 RetryAfterMutation 正交:豁免集修正「哪些调用算变更」的判定输入,
// RetryAfterMutation 决定「已判定变更后是否仍重试」。nil/空=零行为(纯启发式)。
NonMutatingTools map[string]bool
// DisableStreaming 关闭流式(默认 false=开):开流时思考/正文以 *_delta 增量事件
// 外发(真流式观测面),每轮收流后仍按完整消息走既有派发与终稿判定——消费方
// 语义不变,只多出增量通道。模型/网关不兼容流式时的逃生开关。
DisableStreaming bool
// OnUsage 流式调用的每轮用量回调(可选):开流后 eino callbacks OnEnd 拿到的是
// StreamReader(无法在回调侧消费取 Usage),由 reactHandler 收流 concat 后从
// ResponseMeta 提取并回调,每轮一次。非流式路径不触发(消费方沿用既有
// callbacks 用量面板,两路不会双计)。ctx 为 run 的调用 ctx(归因取值用)。
OnUsage func(ctx context.Context, u Usage)
// ModelOptions 每次模型调用的 per-call eino Option(可选,批次 extra_body 透传缝):
// 经 adk.WithChatModelOptions 注入 runner.Query——ADK 图执行时分发到 chat model
// 节点的 Generate/Stream,随装饰链下钻到真实 ChatModel(openai.WithExtraFields
// 等实现专属项即经此生效)。nil/空=零行为(存量路径不变)。流式与非流式两分支
// 共用 Query 入口故一并覆盖;FailoverModel 会把同一批 opts 转发给主/备链——
// 端点特定的扩展字段需调用侧自觉仅对认得它的档位设置(多数 OpenAI 兼容网关忽略
// 未知字段,严格网关可能 400)。
ModelOptions []model.Option
}
Config ReAct agent 运行配置。
type Event ¶
type Event struct {
Type string // reasoning | reasoning_delta | text | text_delta | tool_call | tool_result
Text string // *_delta 时为增量分片(非累积快照)
Tool string // tool_call/tool_result 的工具名
Args string // tool_call 的 JSON 参数串
CallID string // tool_call/tool_result 的原生调用 ID(声明 tc.ID / 结果 ToolCallID)——观测面精确配对依据
}
Event agent 运行过程事件(OnEvent 回调载荷,观测/进度展示用)。
type FinalMissingError ¶
type FinalMissingError struct {
EmptyContent bool
Iterations int
LastReasoning string // 最后一轮 assistant 消息的 reasoning_content(可能为空)
}
FinalMissingError 终稿缺失错误(可重试形态):迭代耗尽未出口(EmptyContent=false) 或最终消息正文为空(EmptyContent=true——推理型模型把终稿只写进思考过程的形态, 2026-10-13 bianque kylin-insight 实弹:qwen3.8-flash 12.7k 字分析全在 reasoning, content 空 → 整程重跑 32 次只读调用重放)。携带最后一段思考片段供重试侧救回: 分析已完成时重试查询附上存档,模型直接产出终稿,不再从头重放工具采集。
func (*FinalMissingError) Error ¶
func (e *FinalMissingError) Error() string
type PlanExecuteConfig ¶
type PlanExecuteConfig struct {
// Planner 规划模型(须支持 tool calling——eino-ext openai ChatModel 均满足)。
// 计划结构经默认 Plan ToolInfo 以 tool-calling 形态强制产出。
Planner model.ToolCallingChatModel
// Replanner 重规划/完成判定模型(须支持 tool calling;空 = 复用 Planner
// 模型——eino planexecute.Config 的 Replanner 是必填项,agentrun 在此
// 兜底装配。生产可配更便宜的模型跑判定)。
Replanner model.ToolCallingChatModel
// Executor 执行模型(带工具时同样须支持 tool calling)。
Executor model.ToolCallingChatModel
// Tools 执行器可用工具(nil = 纯推理执行)。
Tools []tool.BaseTool
// ToolAliases 工具**名别名**表(可选):语义同 Config.ToolAliases——
// 键=规范名(`tool.Info().Name`,MCP 场景为 `server.工具名`),
// 值=别名配置;模型用别名调用时解析回规范名。
// MCP 暴露名带 server 命名空间、模型常发裸名 ⇒ 不注册别名则该步整步失败
// (eino 报 `tool ... not found in toolsNode indexes`)。
// nil/空=零行为(存量路径不变)。
ToolAliases map[string]compose.ToolAliasConfig
// UnknownToolsHandler 幻觉工具兜底(可选):语义同 Config.UnknownToolsHandler——
// 模型调用不存在的工具时由它接管,返回的字符串作为该次调用的结果回给模型。
// nil/空=零行为(存量路径不变:eino 侧未设时直接报错)。
UnknownToolsHandler func(ctx context.Context, name, input string) (string, error)
// MaxSteps "执行-重规划"循环上限(默认 10)。
MaxSteps int
// PlannerInstruction 追加给规划器的约束/背景(可空;经输入整形注入,
// 不覆盖 planexecute 默认的计划格式提示词)。
PlannerInstruction string
}
PlanExecuteConfig Plan-and-Execute 运行配置。
type PlanExecuteResult ¶
type PlanExecuteResult struct {
// Text 最终答复(Replanner 判定任务完成时产出)——与 acpx.RunResult.Text、
// reflection.RefineResult.Text 同词表。
Text string
}
PlanExecuteResult 运行结果。
func PlanAndExecute ¶
func PlanAndExecute(ctx context.Context, cfg PlanExecuteConfig, goal string) (*PlanExecuteResult, error)
PlanAndExecute 运行 Plan-and-Execute。
type RunOption ¶
type RunOption func(*runOpts)
RunOption Run 入口的可选行为(v0.10.38 破坏式收敛:此前 Run/RunWithEvents/ RunWithRetry/RunWithEventsAndRetry 四个导出入口层层包裹——retryQuery 是 独立参数而 RetryAfterMutation 却在 Config,签名不对称;入口唯一后行为 正交组合,守卫逻辑只此一份)。
func WithOnEvent ¶
WithOnEvent 过程事件回调(可多传,末个生效;nil 回调等价不传)。 契约:回调在事件 drain 循环内同步执行——不得阻塞(阻塞会拖停整轮 agent)、 不得 panic(会击穿调用方 goroutine,与 acpx OnEvent 同契约;事件已固化, 回调内消费失败不影响事件面完整性,自行 recover)。
func WithRetry ¶
WithRetry 失败回喂重试一次:首次失败(或产出空文本)时以 retryQuery 再跑, 两次均失败返回末次错误。副作用守卫:首轮已调用变更类工具(见 IsMutatingTool)后不整体重跑,除非 Config.RetryAfterMutation=true——重跑会 重复副作用(脚本执行/服务操作类工具在首轮已生效)。 ⚠ retryQuery 是**完整替换** query 而非追加(2026-09-19 bianque 实弹教训: 消费者若传纯提示语,重试轮将丢失全部任务材料——工单/合议上下文全空,模型 输出「未收到输入」类空心合规报告)。消费方应传 query+提示语的拼接串。 注意:两次尝试共用 cfg.Tools 实例——工具表含 toolprior.LimitCalls 等有状态 包装时,限流计数会跨尝试累计;需要按尝试重置预算请设置 ToolsFactory。
type SideEffectTracker ¶
type SideEffectTracker struct {
// contains filtered or unexported fields
}
SideEffectTracker 观测一次 agent 运行中是否调用过变更类工具(事件回调包装器, 观测不截断:Wrap 后事件照常透传给原回调)。exempt 为工具名豁免集 (Config.NonMutatingTools 注入;零值=无豁免,纯动词启发式)。
func (*SideEffectTracker) MutatingTool ¶
func (t *SideEffectTracker) MutatingTool() string
MutatingTool 首个被调用的变更类工具名(未调用返回 "")。
func (*SideEffectTracker) Observe ¶
func (t *SideEffectTracker) Observe(ev Event)
Observe 处理一个运行事件(接 OnEvent 回调)。
func (*SideEffectTracker) Wrap ¶
func (t *SideEffectTracker) Wrap(onEvent func(Event)) func(Event)
Wrap 包装事件回调:记录变更类工具调用并透传事件(onEvent 可为 nil)。
type Usage ¶
type Usage struct {
PromptTokens int
CachedTokens int
CompletionTokens int
ReasoningTokens int
TotalTokens int
FinishReason string
// DurationMS 本轮 LLM 调用的墙钟耗时(毫秒)——流式路径自模型流交付
// (拿到本轮 ResponseMeta 的流)到收流 concat 完成的时长,口径与
// llm.UsageRecord.DurationMS 同义(time.Since(start).Milliseconds())。
//
// 0 = 未采集。消费方据此区分「零耗时」与「没采到耗时」——字段缺失时两者
// 不可区分(消费侧只能记 0,看板上表现为「数据全 0」,比没有数据更坏)。
//
// 口径边界:起点是模型流交付时刻,**不含**「请求发出到首帧到达」的排队
// 与首字延迟(TTFB);终点是末帧收齐并 concat 完成的时刻。
DurationMS int64
}
Usage 单轮 LLM 调用用量(流式 OnUsage 回调载荷;字段与 eino TokenUsage 同义)。