Back to Weknora

Agent 引擎

website-docs/03-features/07-agent.md

0.7.256.0 KB
Original Source

Agent 引擎

普通问答是「检索一次、回答一次」,遇到需要多步骤的问题就不够用了——比如「对比这三份合同的付款条款,并查一下最新的行业惯例」。Agent 解决的是这类问题:它会自己决定检索几轮、要不要联网、要不要调外部工具,边想边做,直到攒够依据再回答。

WeKnora 提供两种模式,在对话框顶部切换:

模式适合代价
快速问答(quick-answer)事实性提问,答案就在文档里一轮检索,快、便宜
智能推理(smart-reasoning)需要多步骤、跨文档、要联网或调工具多轮模型调用,慢、贵

除内置 Agent 外,可以在「智能体」页建自己的 Agent:选模式与模型、圈定可用知识库、开关联网搜索、挂载 MCP 工具与技能、写专属提示词。建好的 Agent 可以在网页对话里用,也可以绑到 IM 渠道或网页挂件上对外服务。

<Screenshot src="/screenshots/agent-editor.png" caption="自定义 Agent 配置:模式、模型、知识范围与工具" hint="展示 Agent 编辑弹窗,含模式选择、模型选择、知识库范围、联网搜索开关与 MCP 工具勾选。" />

<Screenshot src="/screenshots/agent-chat.png" caption="Agent 对话:推理过程与工具调用时间线" hint="展示一轮 Agent 回答,包含展开的思考步骤、工具调用卡片与最终答案的引用。" />

下文依次介绍 Agent 引擎的整体架构、ReAct 循环、全部内置工具、记忆与上下文压缩、技能系统与沙箱、工具审批、自定义/内置 Agent 配置,以及 Agent 模式与普通 RAG 问答模式的关系。

1. 总览与架构

1.1 核心组件

组件源码位置职责
AgentEngineinternal/agent/engine.goReAct 主循环的驱动者,持有配置、工具注册表、Chat 模型、事件总线等
ToolRegistryinternal/agent/tools/registry.go工具注册、查找、参数校验、执行、输出截断、资源清理
内置工具集internal/agent/tools/*.go24 个内置工具 + 动态注册的 MCP 工具
Token 估算与压缩internal/agent/token/Estimator(BPE 估算)与 CompressContext(滑动裁剪)
记忆整合internal/agent/memory/consolidator.goLLM 驱动的历史摘要(Memory Consolidation)
技能系统internal/agent/skills/SKILL.md 的发现、加载与脚本执行(Progressive Disclosure)
执行沙箱internal/sandbox/技能脚本的 Docker / Local 隔离执行与安全校验
工具审批internal/agent/approval/gate.goMCP 危险工具的人工审批(HITL)与会话内 OAuth 授权
Agent 服务层internal/application/service/agent_service.go组装引擎:注册工具、解析 KB 元信息、初始化技能/沙箱/VLM
会话问答入口internal/application/service/session_agent_qa.goCustomAgent 构建运行时 AgentConfig 并执行
历史重建internal/application/service/agent_history.go从 DB 重建多轮 LLM 上下文(LoadAgentHistory

AgentEngine 的结构体定义(internal/agent/engine.go):

go
type AgentEngine struct {
	config               *types.AgentConfig
	toolRegistry         *agenttools.ToolRegistry
	chatModel            chat.Chat
	eventBus             *event.EventBus
	knowledgeBasesInfo   []*KnowledgeBaseInfo      // Detailed knowledge base information for prompt
	selectedDocs         []*SelectedDocumentInfo   // User-selected documents (via @ mention)
	pinnedMCPServices    []*PinnedMCPServiceInfo   // User @mentioned MCP services for this turn
	pinnedSkills         []*PinnedSkillInfo        // User @mentioned skills for this turn
	sessionID            string
	systemPromptTemplate string
	skillsManager        *skills.Manager           // Skills manager for Progressive Disclosure (optional)
	appConfig            *appconfig.Config
	imageDescriber       ImageDescriberFunc        // VLM function for describing images in tool results
	tokenEstimator       *agenttoken.Estimator     // Token estimator for context window management
	memoryConsolidator   *agentmemory.Consolidator // Memory consolidator for LLM-powered summarization
	lastUsage            types.TokenUsage          // Token usage from the most recent LLM call
	lastSentMsgCount     int
	resourceRefs         *llmresource.Registry
	sourceRefs           *llmreference.Registry
}

几个关键设计点:

  1. 引擎跨轮无状态(stateless across turns)。引擎源码注释明确写道:会话历史每轮由调用方通过 service.LoadAgentHistory 从 DB 重建,作为 llmContext 传入 Execute;引擎自身不维护缓存、system prompt 存储或跨轮缓冲。
  2. 事件驱动输出。引擎不直接写 SSE,所有输出(思考、工具调用、工具结果、最终答案、完成事件)都通过 event.EventBus 发射,由 Handler 层的订阅者转成 SSE 流并落库。相关事件类型包括 EventAgentThoughtEventAgentFinalAnswerEventAgentToolCallEventAgentToolResultEventAgentToolEventAgentCompleteEventError
  3. 引用/资源别名resourceRefsllmresource.Registry)与 sourceRefsllmreference.Registry)在每次 LLM 调用前对消息做 Encode,把持久化 ID(chunk/document/web 的 UUID)替换为短别名(cN/dN/bN/wNres://NNNN),流式返回时再 Decode。这样模型永远看不到真实 UUID。think.go 中特别注明了编码顺序:resourceRefs 必须先于 sourceRefs 编码,否则 wiki summary 页 slug 中内嵌的文档 UUID 会被 citation 压缩误替换为 d1 之类的别名,形成死链。
  4. 可观测性。每次执行会开启 Langfuse span 层级:agent.executeagent.round.Nagent.tool.<name>,内含轮次、token 用量、工具输出预览(截断至 4000 rune)等。database_query 的 SQL 参数在 Langfuse 与 UI hint 中均被脱敏(toolHintSensitiveArgs)。

1.2 组件关系图

mermaid
flowchart TB
    subgraph HandlerLayer["Handler 层"]
        H1["session/qa.go AgentQA"]
        SSE["SSE 流 / agent_stream_handler"]
    end
    subgraph ServiceLayer["Service 层"]
        SQA["session_agent_qa.go
buildAgentConfig + LoadAgentHistory"]
        AS["agent_service.go
CreateAgentEngine / registerTools"]
    end
    subgraph EngineLayer["internal/agent"]
        ENG["AgentEngine
(ReAct 主循环)"]
        TOK["token.Estimator + CompressContext"]
        MEM["memory.Consolidator"]
        REG["tools.ToolRegistry"]
    end
    subgraph Tools["工具集"]
        KB["KB 检索工具
knowledge_search / grep_chunks / ..."]
        WIKI["Wiki 工具 x10"]
        WEB["web_search / web_fetch"]
        DATA["data_schema / data_analysis(DuckDB)"]
        SKILL["read_skill / execute_skill_script"]
        MCP["MCP 工具 mcp_{service}_{tool}"]
    end
    GATE["approval.Gate
(HITL 审批 / OAuth)"]
    SBX["sandbox.Manager
(Docker / Local)"]
    EB["event.EventBus"]

    H1 --> SQA --> AS --> ENG
    ENG --> TOK
    ENG --> MEM
    ENG --> REG
    REG --> KB
    REG --> WIKI
    REG --> WEB
    REG --> DATA
    REG --> SKILL
    REG --> MCP
    MCP --> GATE
    SKILL --> SBX
    ENG --> EB --> SSE

1.3 System Prompt 的构建

internal/agent/prompts.go 中的 BuildSystemPromptWithOptions 按以下优先级选择模板:

  1. Agent 配置了自定义 system prompt(AgentConfig.UseCustomSystemPromptSystemPrompt 非空)→ 直接使用;
  2. 无任何绑定知识库 → GetPureAgentSystemPromptconfig/prompt_templates/agent_system_prompt.yaml 中 mode 为 pure 的模板);
  3. 否则 → GetProgressiveRAGSystemPrompt(mode 为 rag 的模板)。

模板支持的占位符(renderPromptPlaceholdersWithStatus):

占位符展开为
{{knowledge_bases}}历史遗留占位符;现在展开为一句指向 <runtime_context><bound_knowledge_bases> 的提示(KB 详情已移入用户消息)
{{web_search_status}}Enabled / Disabled
{{current_time}}RFC3339 当前时间
{{language}}用户语言名(如 "Chinese (Simplified)")
{{skills}}被清空;技能元数据由 formatSkillsMetadata 单独追加

启用技能时,formatSkillsMetadata 会在 system prompt 末尾追加 "Available Skills" 段落(Level 1 元数据 + 强制的 Skill Matching Protocol),并说明 read_skill / execute_skill_script 两个工具的用法。

运行时上下文(runtime_context):与 system prompt 不同,绑定 KB 的完整详情(capabilities、最近文档/FAQ 列表)、@提及的固定文档(pinned_documents)、当前时间、会话 ID,是以 XML 块 <runtime_context scope="this_turn"> 注入到当前轮用户消息里的(internal/agent/observe.gobuildRuntimeContextBlock),且不持久化到历史,避免过期 scope 干扰后续轮次。块内还固定携带两条指令:

  • <communication_instruction>:禁止在答案/思考中出现内部工具名和内部 ID(要求说"关键词检索"而非 grep_chunks 等);
  • <answer_instruction>:信息足够后直接以纯文本写出完整答案并停止(不要再发起工具调用)——这就是 Agent 的终止协议。

当用户 @提及了 MCP 服务或技能时,buildMustUseBlock 会额外注入 <must_use> 块,强制模型使用对应前缀的 MCP 工具或先 read_skill

2. ReAct 循环逐阶段详解

2.1 入口:Execute

AgentEngine.Executeinternal/agent/engine.go)流程:

  1. defer e.toolRegistry.Cleanup(ctx) —— 执行结束时清理实现了 types.Cleanable 的工具(如 data_analysis 会 DROP 本会话建的 DuckDB 表);
  2. 开启 Langfuse agent.execute span;
  3. 初始化 types.AgentStateRoundStepsKnowledgeRefsIsComplete=falseCurrentRound=0);
  4. buildSystemPrompt + buildMessagesWithLLMContext(system + 历史 + 当前用户消息,附图片 URL);
  5. buildToolsForLLM 把注册表中的工具转换为 function calling 定义;
  6. 进入 executeLoop

2.2 主循环:executeLoop 与 runReActIteration

go
for state.CurrentRound < e.config.MaxIterations {
    // ctx 取消检查 → 若已有工具结果则抢救性合成最终答案
    outcome, iterErr := e.runReActIteration(...)
    switch outcome {
    case iterOutcomeContinue: continue loop   // 空回复重试,不消耗轮次
    case iterOutcomeBreak:    break loop      // 终止(自然停止/卡死/取消/内容过滤)
    case iterOutcomeNext:     state.CurrentRound++
    }
}
if !state.IsComplete && ctx.Err() == nil {
    e.handleMaxIterations(ctx, query, state, sessionID) // 兜底合成最终答案
}

executeLoopdefer emitCompletion() 保证每条退出路径恰好发射一次 EventAgentComplete(使用 context.WithoutCancel 使用户点击"停止"后事件仍能送达),该事件携带 state.RoundSteps,由 stream handler 写到 assistant 消息的 AgentSteps 字段持久化。

一次迭代 runReActIteration 内部依次是四个阶段:

① Think(思考):先做上下文窗口管理(见第 4 节),然后 callLLMWithRetryinternal/agent/think.go):

  • agenttools.SanitizeMessages 修复连续同角色、孤儿 tool result 等问题;
  • 流式调用 LLM(streamThinkingToEventBus),单次调用超时 defaultLLMCallTimeout = 120s(可用 AgentConfig.LLMCallTimeout 覆盖);
  • 瞬时错误(429/5xx/timeout/overloaded 等,见 transientErrorMarkers)最多重试 maxLLMRetries = 2 次,退避 1s、2s;
  • 若重试仍失败但此前已有工具结果,走优雅降级streamFinalAnswerToEventBus 基于既有工具结果合成最终答案,state.IsComplete = true

流式过程中:reasoning_content 通道(DeepSeek 等)与内嵌 <think> 块(由 ThinkStreamSplitter 切分)都路由到"思考"区(EventAgentThought);普通 content 直接乐观地流到最终答案区(EventAgentFinalAnswer),如果本轮随后发起了工具调用,这段文本会被 UI 视为 preamble 挪进步骤树,同时保留为该轮的 Thought

② Analyze(判定)analyzeResponseinternal/agent/observe.go)检查停止条件:

  • finish_reason == "content_filter" 且无工具调用 → 终止,答案为被过滤的内容或固定的道歉话术;
  • 自然停止(isNaturalStopFinishReasonstop / end_turn / stop_sequence)且无工具调用 → Agent 结束,纯文本回复即最终答案(没有专门的 final_answer 工具;历史数据中遗留的 final_answer 工具调用会在重放时被 filterNonTerminalToolCalls 过滤掉);
  • 自然停止但内容为空 → 追加一条 nudge 用户消息 "Please provide your complete answer now as plain text." 重试,最多 maxEmptyResponseRetries = 2 次(返回 iterOutcomeContinue,不消耗轮次);重试耗尽用固定 fallback 文案终止。

另有一个卡死检测在 Analyze 之前:若连续 maxRepeatedResponseRounds = 2 轮返回完全相同内容且无工具调用(通常是未处理的 finish reason 导致),强制终止并把该内容作为最终答案。

③ Act(行动)executeToolCallsinternal/agent/act.go)执行本轮所有工具调用:

  • AgentConfig.ParallelToolCalls == true 且调用数 ≥ 2 时用 errgroup 并行执行(best-effort,单个失败不取消兄弟任务),结果按原顺序回填;
  • 每个调用先 NormalizeToolCallID,然后解析 JSON 参数——解析失败会先经 RepairJSON 修复再试;仍失败则返回带提示的错误结果("[Analyze the error above and try a different approach.]"),让模型换路子而不是让整轮失败;
  • 单个工具执行超时 defaultToolExecTimeout = 60sToolExecContext 中额外携带不带该超时的 ApprovalCtx,供 MCP 人工审批/OAuth 等合法长等待使用;
  • 发射 EventAgentToolCall(含中文 display name 的 hint,如 搜索网页("..."))、EventAgentToolResultEventAgentTool 事件。

④ Observe(观察)appendToolResultsinternal/agent/observe.go)按 OpenAI 协议把本轮追加进消息数组:一条带 tool_calls 的 assistant 消息 + 每个结果一条 role:"tool" 消息(内容经 sourceRefs.ModelOutput 别名化)。若本轮任一成功的工具结果里含 Markdown 图片,还会向 system 消息追加一次 ## Retrieved Image Output Requirement 要求(internal/agent/image_requirement.go),强制最终答案原样携带相关图片。随后 state.CurrentRound++ 进入下一轮。

2.3 终止条件汇总与最大迭代

终止路径触发条件最终答案来源
自然停止finish_reason ∈ {stop, end_turn, stop_sequence} 且无工具调用、内容非空该轮纯文本回复
空回复耗尽自然停止但内容为空,nudge 重试 2 次仍空固定 fallback 文案
内容过滤finish_reason == content_filter 且无工具调用被过滤内容或安全提示
卡死检测连续 2 轮相同内容且无工具调用重复的内容本身
用户取消 / 超时ctx.Done();若已有工具结果则抢救合成合成答案或保留部分步骤
LLM 不可恢复失败重试耗尽;有工具结果 → 降级合成,否则报错合成答案 / 错误事件
达到最大迭代CurrentRound == MaxIterationshandleMaxIterationsstreamFinalAnswerToEventBus 合成

最大迭代次数的多层默认值:

  • 引擎级默认 DefaultAgentMaxIterations = 20internal/agent/const.go);
  • 服务层 ValidateConfig<= 0 时兜底为 5,硬上限 MAX_ITERATIONS = 100internal/application/service/agent_service.go);
  • CustomAgent.EnsureDefaults:未配置时为 10(internal/types/custom_agent.go);
  • 内置 Agent:智能推理 50、数据分析师 30、Wiki 问答/修订 30(config/builtin_agents.yaml)。

达到上限后 handleMaxIterations 会用一个专门的合成 prompt(internal/agent/finalize.go)把全部工具结果作为 user 消息喂给 LLM 生成完整答案(合成阶段关闭 thinking),若检索结果含 Markdown 图片还会附加图片输出要求。

2.4 ReAct 循环流程图

mermaid
flowchart TD
    START(["Execute 入口"]) --> INIT["构建 system prompt + 历史消息 + 工具定义"]
    INIT --> CHECK{"CurrentRound < MaxIterations?"}
    CHECK -- "否" --> MAXED["handleMaxIterations:
用工具结果合成最终答案"]
    MAXED --> DONE(["EventAgentComplete"])
    CHECK -- "是" --> CANCEL{"ctx 已取消?"}
    CANCEL -- "是,且已有工具结果" --> SALVAGE["抢救合成最终答案"] --> DONE
    CANCEL -- "否" --> CTXMGMT["上下文窗口管理:
Consolidate(>50% 预算)+ CompressContext(>80% 预算)"]
    CTXMGMT --> THINK["Think:流式调用 LLM
(120s 超时,瞬时错误重试 2 次)"]
    THINK -- "失败且有工具结果" --> SALVAGE
    THINK --> STUCK{"连续 2 轮相同内容
且无工具调用?"}
    STUCK -- "是" --> DONE
    STUCK -- "否" --> ANALYZE{"analyzeResponse 判定"}
    ANALYZE -- "content_filter" --> DONE
    ANALYZE -- "自然停止且内容非空" --> FINAL["纯文本回复 = 最终答案"] --> DONE
    ANALYZE -- "自然停止但内容为空" --> EMPTY{"空回复重试 <= 2?"}
    EMPTY -- "是" --> NUDGE["追加 nudge 用户消息
(iterOutcomeContinue,不消耗轮次)"] --> THINK
    EMPTY -- "否" --> FALLBACK["固定 fallback 文案"] --> DONE
    ANALYZE -- "有工具调用" --> ACT["Act:执行工具调用
(可并行,单工具 60s 超时)"]
    ACT --> OBSERVE["Observe:assistant+tool 消息入上下文,
必要时注入图片输出要求"]
    OBSERVE --> NEXT["CurrentRound++"] --> CHECK

3. 内置工具全解

3.1 工具总表

工具名常量定义在 internal/agent/tools/definitions.go。下表覆盖全部内置工具(参数列只列 schema 中的字段,* 为必填):

工具名关键参数行为 / 返回
thinkingthought*、next_thought_needed*、thought_number*、total_thoughts*、is_revisionrevises_thoughtbranch_from_thoughtbranch_idneeds_more_thoughtsSequential Thinking:记录/修订/分支思考步骤;返回思考进度(含 incomplete_steps),提示禁止在思考里出现工具名和最终答案
todo_writetasksteps[]*(id/description/status:pending/in_progress/completed)创建/更新检索类任务计划,仅限检索任务(总结交给 thinking);返回格式化计划,display_type: "plan"
knowledge_searchqueries[]*(1–5 条语义问题)、knowledge_base_ids[]语义/向量检索,可选 rerank;默认 topK=5、vector 阈值 0.6、keyword 阈值 0.5;minScore 参数虽然仍可传入且默认 0.3,但后置过滤已被跳过——HybridSearch 改用 RRF 融合后分数落在 [0, ~0.033] 区间,旧的 [0,1] 阈值不再适用,阈值过滤在 RRF 之前就已由各引擎完成,重排阶段另有 rerankThreshold()(优先取全局配置);结果带 cN/dN 短 ID;会话内已见 chunk 去重压缩
grep_chunksquery*(单条 POSIX 正则,支持 | 交替)直接在 DB 做大小写不敏感正则匹配(PostgreSQL ~* / MySQL REGEXP);上限 30 条,>10 条时做 MMR(λ=0.7)去冗;返回 <match> 片段、按文档聚合摘要(最多 20 行);已见 chunk 标 already_seen
list_knowledge_chunksfaq_id / chunk_id / knowledge_id(三选一)、limit(默认 20 上限 100)、offset读取单个 FAQ/chunk 或分页遍历某文档全部分块;校验 KB 在 searchTargets 内及 @mention 范围
query_knowledge_graphknowledge_base_ids[]*(1–10 个 bN)、query*并发查询各 KB 知识图谱的实体与关系;未配置图谱的 KB 退化为普通检索结果
get_document_infoknowledge_ids[]dN)、faq_ids[]cN)(至少一个)并发批量返回文档元数据(标题、类型、大小、parse_status、分块数)或 FAQ 标准问/答案
database_querySQL(SELECT-only)只读查询白名单表(knowledge_bases/knowledges/chunks),自动注入 tenant_id 过滤与 deleted_at IS NULL;SQL 参数在 UI/Langfuse 中脱敏
data_schemaknowledge_id*(dN读取 CSV/Excel 文件的 table_summary + table_column 类型分块,返回表名、列信息与行数
data_analysisknowledge_id*、sql*把 CSV/Excel 载入 DuckDB 后执行 SQL;多 Sheet Excel 合并为一张表并暴露 __sheet_name 列;自动纠正列名大小写/空格差异;会话结束 Cleanup 时 DROP 所建表
web_searchquery*联网搜索;描述中强制 "KB First" 规则(必须先 grep_chunks + knowledge_search);结果经 RAG 压缩、缓存进会话级临时知识库,返回 wN 页面短 ID
web_fetchitems[]*(每项 url=wNprompt并发抓取网页(SSRF 安全客户端 + DNS pinning,必要时 chromedp 渲染),抽取正文后用小模型按 prompt 摘要;60s 超时。逐 URL 返回 success/failed/skipped 状态与可重试错误码,部分失败不影响其它页面
read_skillskill_name*、file_path读取技能 SKILL.md 全文(Level 2)或技能目录内指定文件(Level 3),并列出目录内可执行脚本
execute_skill_scriptskill_name*、script_path*、args[]input(stdin)在沙箱中执行技能脚本,返回 stdout/stderr/exit code/duration/killed
wiki_searchqueries[]*(正则)、limit(默认 10)、knowledge_base_id在 Wiki 页面(标题/内容/slug/摘要)上做 POSIX 正则搜索,返回带 bN 标记的页面与摘要;已见 slug 去重
wiki_read_pageslugs[]*、knowledge_base_id按 slug 读取 Wiki 页面全文、元数据、出入链(链接附摘要,已见的省略);index slug 返回按类型分组的目录概览(每类 top 20)
wiki_read_source_docknowledge_id*(dN)、query(正则)、start_chunk_indexend_chunk_index深入阅读 Wiki 页面的源文档:正则过滤或按 chunk 区间取连续内容;都不传则返回文档开头
wiki_write_pageslug*、title*、summary*、content*、page_type*、aliases[]source_refs[]新建或整页覆盖 Wiki 页面;写入前规范化并校验 slug;自动处理出链
wiki_replace_textslug*、old_text*、new_text*、source_refs[]精确文本替换,适合小修订
wiki_rename_pageslug*、new_slug*重命名 slug 并级联更新所有引用它的页面链接
wiki_delete_pageslug*删除页面并自动清理其他页面上的入链,防止死链
wiki_flag_issueslug*、issue_type*(mixed_entities/contradictory_facts/out_of_date/other)、description*、suspected_knowledge_ids[]标记页面事实错误/实体混淆等问题,记录 issue 供人工或自动维护
wiki_read_issueissue_id / slug查看某条 issue 详情或列出某页面的 pending issue
wiki_update_issueissue_id*、status*(resolved/ignored/pending)更新 issue 状态
mcp_{service}_{tool}(动态)由 MCP 服务的 InputSchema 决定包装外部 MCP 工具;描述前缀 [MCP Service: X (external)] 提示不可信来源;可挂人工审批与会话内 OAuth

默认工具白名单 DefaultAllowedTools()(旧 Agent 未配置 allowed_tools 时的回退):thinkingtodo_writeknowledge_searchgrep_chunkslist_knowledge_chunksquery_knowledge_graphget_document_infodatabase_querydata_analysisdata_schema

3.2 工具注册表(ToolRegistry)

internal/agent/tools/registry.go

  • 注册RegisterTool 采用 first-wins 策略——同名工具后注册者被拒绝,防止 MCP 服务通过名字碰撞劫持内置工具(对应安全公告 GHSA-67q9-58vj-32qx);
  • 定义导出GetFunctionDefinitions 按工具名排序,保证发给 LLM 的 tools 载荷跨请求字节级一致,以命中依赖前缀匹配的 provider prompt cache(如 Qwen 显式缓存);
  • 执行管线ExecuteTool = CastParams(把 "true"true 等 LLM 常见类型偏差)→ ValidateParams(按 JSON Schema 预校验,省一次无效执行 + LLM 往返)→ tool.Execute → 输出截断;
  • 输出截断TruncateToolOutputtruncate.go)默认上限 DefaultMaxToolOutput = 16000 rune(可由 AgentConfig.MaxToolOutputChars 覆盖),超限保留头 70% + 尾 30%,中间插入截断标记,防止大结果污染上下文;
  • 错误提示:失败结果统一追加 "[Analyze the error above and try a different approach.]",引导 LLM 换策略;
  • 清理Cleanup 遍历实现 types.Cleanable 的工具释放资源。

3.3 能力(capabilities)机制与按配置启停

internal/agent/tools/capabilities.go 是前端 frontend/src/utils/tool-capabilities.ts 的 Go 镜像,声明每个工具对 KB 能力的需求:

go
var ToolCapabilityRequirements = map[string]ToolRequirement{
	"thinking":   {},
	"todo_write": {},
	"knowledge_search":      {AnyOf: []KBCapability{CapVector, CapKeyword}, ConsumesFiles: true},
	"grep_chunks":           {AnyOf: []KBCapability{CapVector, CapKeyword}, ConsumesFiles: true},
	// ...
	"wiki_search":          {AllOf: []KBCapability{CapWiki}},
	// ...
	"data_analysis": {AnyOf: []KBCapability{CapVector, CapKeyword}, ConsumesFiles: true},
}

能力枚举为 vector / keyword / wiki / graph / faq。由此派生:

  • DeriveKBFilterForAgent(agentMode, allowedTools):Agent 编辑器/@ 菜单里可选 KB 的过滤谓词;quick-answer 模式隐式要求 vector|keyword
  • KBSatisfiesToolRequirements:后端最后防线——绕过前端的客户端也无法把不兼容 KB 塞给工具;
  • ToolsConsumeFiles:决定聊天输入框是否展示 @file 列表。

运行时启停逻辑agent_service.goregisterTools)遵循"只过滤、不注入"原则:

  1. 起点是 config.AllowedTools(用户可编辑的白名单,preset 只做初始填充);为空回退 DefaultAllowedTools()
  2. 若本轮没有任何知识检索 scope(Pure Agent 模式),过滤掉全部 KB/Wiki/数据工具;若同时未开 Web 搜索,连 todo_write 也一并去掉;
  3. WebSearchEnabled 时自动追加 web_search + web_fetch
  4. 硬安全网:扫描 SearchTargets 中各 KB 的真实能力——没有 wiki KB 就丢弃全部 wiki 工具;没有 vector/keyword KB 就丢弃全部 RAG 工具(防止配置陈旧:先勾了 wiki 工具、后换成非 wiki KB);
  5. 去重后逐个实例化并注册;MCP 工具按 MCPSelectionMode(all/selected/none)另行注册;技能工具(read_skillexecute_skill_script)由技能管理器初始化时注册,且 execute_skill_script 仅在沙箱未禁用时注册。

4. 记忆与上下文压缩

4.1 Token 预算与估算器

  • 上下文预算:AgentConfig.MaxContextTokensbuildAgentConfig 未设置时兜底 types.DefaultMaxContextTokens = 200000
  • token.Estimatorinternal/agent/token/estimator.go)用 tiktoken 的 cl100k_base 编码估算,常量 perMessageOverhead = 3perConversationTail = 3;编码失败时退化为 len(s)/4 近似;
  • 权威值优先:真正的 token 数以模型 API 返回的 Usage 为准。引擎的 estimateCurrentTokens 用上一轮 API 报告的 lastUsage.TotalTokens 作基线,只对新增消息(assistant 回复 + tool 结果)做 BPE 增量估算;首轮无 Usage 时才全量估算。

4.2 两级压缩策略

manageContextWindowinternal/agent/observe.go)在每轮 Think 之前执行:

第一级:LLM 记忆整合(memory.Consolidator) —— 当估算 token 超过 MaxContextTokens × 0.5DefaultConsolidationThreshold = 0.5)时触发:

  • 保留:system prompt(首条)、当前轮(最后一条 user 消息及其后全部 assistant/tool 消息)、以及按 token 预算从尾部回收的近期历史(findKeepBoundarytargetTokens = maxTokens × 0.5 × 0.6 为目标,预留 500 token 给摘要,且回收时把 assistant+tool_calls 与其 tool 结果作为整组处理,绝不拆散);
  • 其余较老的历史交给 LLM 摘要(低温 0.3、MaxTokens: 2000、单次 60s 超时、最多 maxConsolidationAttempts = 3 次),摘要要求保留关键事实、工具结果、用户意图和错误处理过程,目标压到原文 30% 以内;
  • 摘要作为一条 system 消息插入:[Memory Summary - N earlier messages consolidated]
  • LLM 三次都失败则退化为 rawArchive(截断的纯文本归档),绝不丢信息地静默失败。

第二级:滑动裁剪(token.CompressContext) —— 无论整合是否发生都会执行,当 token 超过 MaxContextTokens × 0.8DefaultContextThresholdRatio = 0.8)时:

  • 同样保留 system、当前轮尾部;
  • 中间历史经 groupToolMessages 分组(assistant+tool_calls 与后续 tool 结果为一组),从最老的组开始整组丢弃,直到释放的 token 达到 currentTokens - threshold

4.3 会话历史(agent_history)

跨轮历史由 LoadAgentHistoryinternal/application/service/agent_history.go)每轮从 messages 表重建(DB 是唯一事实来源,无 Redis/内存缓存):

  • HistoryTurns × 4(最低 50)条原始消息,按 RequestID 配对 user/assistant,只保留 assistant 已完成(IsCompleted)的完整轮,按时间排序取最近 HistoryTurns 轮;
  • 每轮展开为:user 消息(含图片 caption 与附件 prompt;故意忽略 RenderedContent 快照以避免旧协议污染)→ 每个含工具调用的 AgentStep 展开为 assistant(with tool_calls) + 若干 tool 消息 → 末尾一条规范化最终答案 assistant 消息(剥离 <think> 块);
  • 历史中的 tool 消息内容用 CompactToolOutputForHistoryinternal/agent/tools/persist.go)压缩:带 display_type 的大载荷(如 knowledge_chunks_list 的 chunks、grep_results 的 chunk_results)替换为一行摘要(如 "Listed 20/87 chunks from X (content omitted from history)")。

进入引擎后,buildMessagesWithLLMContext 还会做历史 KB 结果脱敏redactHistoryKBResults):除非 Agent 开启 RetainRetrievalHistory,历史轮次中 KB 类工具(knowledge_searchgrep_chunkslist_knowledge_chunksquery_knowledge_graphget_document_infowiki_searchwiki_read_pagewiki_read_source_doc)的结果一律替换为 "[Previous retrieval result omitted — knowledge base may have changed. Please perform a fresh search.]",强制模型对可能已变更的知识库做新鲜检索。

持久化侧,SanitizeAgentStepsForStorage 在把 AgentSteps 写入 DB / SSE 重放前剥离 LLM-only 大载荷,只留紧凑摘要。

5. 技能(Skills)系统

5.1 技能文件格式

技能是一个目录,核心是 SKILL.md,遵循 Claude 的 Progressive Disclosure(渐进披露)规范(internal/agent/skills/skill.go):

markdown
---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents. Use when ...
---
# PDF Processing
(正文即 Level 2 指令……)
  • Level 1(元数据):frontmatter 中的 name + description,启动时全部注入 system prompt;
  • Level 2(指令):SKILL.md 正文,模型判断匹配后经 read_skill 按需加载;
  • Level 3(资源):目录内其他文件(文档、脚本),经 read_skill(file_path=...)execute_skill_script 使用。

校验规则(Skill.Validate):name ≤ 64 字符,仅允许 Unicode 字母/数字/连字符,禁止保留词 anthropic/claude,禁止 XML 标签;description ≤ 1024 字符、禁止 XML 标签。脚本识别按扩展名(.py/.sh/.bash/.js/.ts/.rb/.pl/.php)。

5.2 存放位置与加载

位置内容用途
skills/preloaded/citation-generator(引用生成器)、data-processor(数据处理器,含 analyze.py 等脚本)、doc-coauthoring(文档协作)、document-analyzer(文档分析器)、openmaic-classroom(互动课程生成)服务端预置技能,Agent 可勾选
examples/skills/pdf-processing/SKILL.md + scripts/analyze_form.pyscripts/extract_text.py自定义技能示例
cli/skills/weknora-sharedweknora-rag-search(经 //go:embed 打进 CLI 二进制,weknora skills install 释放)面向外部 Agent 使用 WeKnora CLI 的技能

预置目录解析顺序(getPreloadedSkillsDirinternal/application/service/skill_service.go):WEKNORA_SKILLS_DIR 环境变量 → 可执行文件旁的默认目录 → 当前工作目录 → 相对默认路径。

加载链路:skills.Loader.DiscoverSkills 扫描各技能目录下含 SKILL.md 的子目录,解析 frontmatter 缓存元数据;Manager 负责 enabled 开关、allowedSkills 白名单过滤、LoadSkill(Level 2)、ReadSkillFile/ListSkillFiles(Level 3,带路径穿越防护:Clean 后拒绝 .. 与绝对路径,并校验最终绝对路径仍在技能目录内)。

Agent 侧的启停在 configureSkillsFromAgentinternal/application/service/session_agent_qa.go):

  • 沙箱关闭(WEKNORA_SANDBOX_MODE 为空或 disabled)时技能整体不可用
  • SkillsSelectionModeall = 全部预置技能、selected = SelectedSkills 白名单、none/空 = 禁用;
  • 用户 @技能 提及会经 applyPerRequestSkillScope 把本轮白名单收窄到提及集合,并作为 PinnedSkillInfo 注入 <must_use> 块("Must call read_skill(...) before answering")。

5.3 与沙箱(internal/sandbox)的关系

execute_skill_scriptskills.Manager.ExecuteScriptsandbox.Manager.Execute。沙箱由环境变量配置:

环境变量含义默认
WEKNORA_SANDBOX_MODEdocker / local / disableddisabled
WEKNORA_SANDBOX_DOCKER_IMAGEDocker 沙箱镜像wechatopenai/weknora-sandbox:latest
WEKNORA_SANDBOX_TIMEOUT执行超时(秒)60

Manager 与校验器internal/sandbox/manager.govalidator.go):每次执行前,除非 SkipValidationScriptValidator 会做四类静态校验,任一命中即拒绝执行并返回 ErrSecurityViolation

  1. 脚本内容:危险命令黑名单(rm -rf /mkfsdd if=/dev/zero 等)、危险模式正则、网络访问特征(curl/wget/nc/requests.get/fetch(/axios 等)、反弹 shell 模式;
  2. 参数:shell 运算符(&&;|、重定向、换行等)与命令替换(`cmd`$(cmd))注入检测;
  3. stdin:内嵌 shell 命令检测;
  4. 合并入口 ValidateAll

Docker 沙箱docker.godocker run --rm 隔离):

  • --user 1000:1000 非 root、--cap-drop ALL--security-opt no-new-privileges--pids-limit 100
  • 默认 --network none(除非 AllowNetwork);
  • 资源限额:内存默认 DefaultMemoryLimit = 256MB--memory + --memory-swap 同值禁 swap)、CPU 默认 DefaultCPULimit = 1.0 核;
  • 技能目录以只读挂载到 /workspace;可选 --read-only 根文件系统 + 64MB noexec tmpfs;
  • 按扩展名选择解释器(.pypython3 等)。

Local 沙箱local.go / local_unix.go,Docker 不可用时的回退):解释器白名单(默认 python/python3/node/bashcat/grep 等安全命令)、脚本必须为绝对路径且可选限制在 AllowedPaths 内、最小化环境变量、Setpgid 建进程组以便超时后 SIGKILL 整组杀掉。

Manager 初始化时:docker 模式先探测 docker version,可用则异步预拉镜像,不可用且允许回退则降级 local;disabled 模式的 disabledSandbox 拒绝一切执行。

5.4 技能执行时序图

mermaid
sequenceDiagram
    participant LLM as "LLM(ReAct 循环)"
    participant ENG as AgentEngine
    participant SK as skills.Manager
    participant VAL as ScriptValidator
    participant SBX as "Sandbox(Docker / Local)"

    Note over LLM: system prompt 含全部技能
Level 1 元数据(name + description)
    LLM->>ENG: "tool_call: read_skill(skill_name)"
    ENG->>SK: "LoadSkill → SKILL.md 正文 + 文件列表"
    SK-->>LLM: "Level 2 指令(含可执行脚本清单)"
    LLM->>ENG: "tool_call: execute_skill_script(skill, script, args, input)"
    ENG->>SK: ExecuteScript
    SK->>SK: "白名单检查 + LoadSkillFile(路径穿越防护,IsScript 校验)"
    SK->>SBX: "Manager.Execute(ExecuteConfig)"
    SBX->>VAL: "ValidateScript / ValidateArgs / ValidateStdin"
    alt 校验失败
        VAL-->>LLM: "ExitCode=-1, ErrSecurityViolation"
    else 校验通过
        SBX->>SBX: "docker run --rm --network none --cap-drop ALL ...
或本地白名单解释器 + 进程组"
        SBX-->>LLM: "stdout / stderr / exit_code / duration / killed"
    end

6. 工具审批机制(Human-in-the-Loop)

审批代码在 internal/agent/approval/gate.go(issue #1173)。要点:

审批范围:审批门(approval.MCPApproval只接入 MCP 工具——MCPTool.Executeinternal/agent/tools/mcp_tool.go)在真正调用 MCP 服务前询问 gate.NeedsApproval(tenantID, serviceID, toolName);内置工具不走审批。哪些 MCP 工具需要审批由 Checker(DB 中的 MCPToolApprovalService,经 approval.Adapter 适配)按租户+服务+工具名判定。

Fail-close 默认NeedsApproval 的检查器出错时默认要求审批(对 HITL 特性更安全);可用环境变量 WEKNORA_AGENT_TOOL_APPROVAL_FAIL_OPEN=true 恢复旧的放行行为。

审批流程RequestAndWait):

  1. 生成 pendingID(UUID),把 waiter 挂入内存 map;
  2. 通过 EventBus 发射 EventToolApprovalRequired(携带服务名、MCP 工具名、参数 JSON、超时秒数、tool_call_id 等),前端弹出审批卡片;
  3. 阻塞等待三者之一:用户 Resolve、超时(默认 10 分钟cfg.Agent.ToolApprovalTimeoutSeconds 可配)、请求 ctx 取消;结果统一以 EventToolApprovalResolved 通知 UI;
  4. Decision 支持 ApprovedReason,以及 ModifiedArgs——用户可在批准时修改工具参数,MCPTool 会用修改后的参数重新解析执行;
  5. 拒绝/超时/取消都会作为工具失败结果返回给 LLM(而非中断整个 Agent)。

长等待与超时的配合:普通工具执行有 60s 超时,但审批可能等更久。引擎在 ToolExecContext.ApprovalCtx 中传入不含 per-tool 超时的轮级 ctx 供审批等待使用;批准后 MCPTool 再从 ApprovalCtx 派生一个全新的执行超时窗口,避免审批耗尽预算导致刚批准就超时。

跨实例支持:waiter 存在发起等待的实例内存里;配置 Redis 后,Resolve 在本地未命中时通过 Pub/Sub 频道 weknora:mcp_approval:resolve(可加 WEKNORA_REDIS_NAMESPACE 后缀隔离多部署)广播到所有副本,由持有 waiter 的实例投递,并经带 nonce 的 per-pending 回复频道回 ack,使 HTTP 层能准确区分 ok / not_found / tenant_mismatch / user_mismatch / already_resolved。无 Redis 时退化为单进程语义(需要粘性会话)。

授权校验Resolve 时校验 tenant 匹配;waiter 注册了 userID 时调用者必须携带相同的非空 userID(空视为不匹配,fail-close),防止旁人替会话主人批准。

会话内 OAuth:同一个 Gate 还提供 RequestOAuthAndWait——当 MCP 传输层返回"需要授权"错误时(而非查审批表),发射 EventMCPOAuthRequired 让用户在对话内完成 OAuth,等待上限取 Agent 配置的 MCPAuthWaitTimeoutinternal/agent/tools/mcp_oauth.go),授权成功后自动重试工具调用。

7. 自定义 Agent

7.1 模式与类型预设

CustomAgentinternal/types/custom_agent.go)有两个运行模式(Config.AgentMode):

  • quick-answer:经典 RAG 管道(检索→拼上下文→单次生成),不进 Agent 引擎;
  • smart-reasoning:ReAct Agent 模式,IsAgentMode() 返回 true,并强制 MultiTurnEnabled = true

smart-reasoning 下还可选类型预设Config.AgentType,定义在 config/agent_type_presets.yaml,由 internal/types/agent_type_preset.go 加载)。预设只在编辑器里预填表单,用户可任意覆盖:

预设 ID系统提示词模板温度最大迭代预填工具KB 过滤
rag-qaprogressive_rag_agent0.730knowledge_search、grep_chunks、list_knowledge_chunks、get_document_info由工具派生:any_of vector/keyword
wiki-qawiki_researcher0.730wiki_search、wiki_read_page、wiki_read_source_doc、wiki_flag_issue由工具派生:any_of wiki
hybrid-rag-wikihybrid_rag_wiki_agent0.740wiki_search、wiki_read_page、knowledge_search、grep_chunks、list_knowledge_chunks、get_document_info、wiki_flag_issueany_of vector/keyword/wiki
data-analysisdata_analyst0.330data_schema、data_analysis;关闭 web 搜索;限定文件类型 csv/xlsx显式 none_of: [faq]
custom不预填不限制

注意 thinking / todo_write 被有意排除在各预设默认工具之外(token 开销大,需要时手动勾选)。

7.2 可配置项(CustomAgentConfig)

internal/types/custom_agent.goCustomAgentConfig 的主要字段(handler CreateAgent/UpdateAgent 直接接收该结构):

分类字段说明 / 默认(EnsureDefaults)
基础agent_modequick-answer / smart-reasoning
基础agent_typesmart-reasoning 下的预设类别,空/未知视为 custom
基础system_prompt / system_prompt_id直接内容或模板 ID(启动时经 ResolveBuiltinAgentPromptRefs 等解析)
基础context_template / context_template_id普通模式下检索片段的拼装模板
模型model_idrerank_model_idtemperaturemax_completion_tokensthinkingcitation_enabledtemperature<0 → 0.7;max_completion_tokens 默认 2048;thinking 未设时固定为 false;citation 未设时视为 true
Agentmax_iterations默认 10(服务层上限 100)
Agentllm_call_timeout单次 LLM 调用秒数,0 用全局默认(120s)
Agentallowed_tools工具白名单;空回退 DefaultAllowedTools
MCPmcp_selection_mode(all/selected/none)、mcp_servicesmcp_auth_wait_timeoutOAuth 等待秒数 <=0 用 Gate 默认
技能skills_selection_mode(all/selected/none)、selected_skills沙箱禁用时强制不可用
知识库kb_selection_mode(all/selected/none)、knowledge_basesretrieve_kb_only_when_mentionedretain_retrieval_historyretain=true 时历史 KB 检索结果不脱敏
多模态image_upload_enabledvlm_model_idaudio_upload_enabledasr_model_idimage_storage_providerVLM 也用于 MCP 工具返回图片的描述
文件supported_file_typeschat_parser_engine_rulesattachment_image_understandingattachment_ocr_max_pagesattachment_parse_wait_timeout_sec数据分析型 Agent 常限定 csv/xlsx
FAQfaq_priority_enabledfaq_direct_answer_thresholdfaq_score_boost
Webweb_search_enabledweb_search_max_resultsweb_search_provider_idweb_fetch_enabledweb_fetch_top_nmax_results 默认 5
多轮multi_turn_enabledhistory_turnshistory_turns 默认 5;smart-reasoning 强制 multi_turn
检索embedding_top_k(10)、keyword_threshold(0.3)、vector_threshold(0.5)、rerank_top_k(5)、rerank_threshold括号内为默认值
高级enable_query_expansionenable_rewriterewrite_prompt_*query_understand_model_idfallback_strategy(默认 model)、fallback_responsefallback_promptintent_promptsdata_analysis_enabled主要作用于 quick-answer 管道
建议question_suggestions(starters / follow_ups)starters 默认 hybrid 模式 6 条;follow_ups 默认关闭、3 条

Handler 层(internal/handler/custom_agent.go)提供 CreateAgentGetAgentListAgentsUpdateAgentDeleteAgentCopyAgentGetPlaceholders(返回 types.PlaceholdersByField(PromptFieldAgentSystemPrompt) 的占位符清单)、GetAgentTypePresets(带 i18n 的预设列表)、GetSuggestedQuestions。创建/更新时经 authorizeAgentKnowledgeScope 校验受限 API Key 的 KB 范围:kb_selection_mode: all 对 KB 受限 key 直接 403,selected 逐一鉴权。

运行时映射:buildAgentConfigsession_agent_qa.go)把 CustomAgentConfig 转换为引擎的 types.AgentConfiginternal/types/agent.go),并叠加:web 搜索需 Agent 与请求同时开启(customAgent.Config.WebSearchEnabled && req.WebSearchEnabled)、web provider 回退租户默认、SearchTargets 由 KB/@文档/@标签 scope 统一构建、MaxContextTokens 兜底 200000、@Skill/@MCP 的每轮 pin 收窄(共享 Agent 的 @MCP 只能落在 Agent 预设集合内)。另外只有当 knowledge_search 实际可用时才要求配置 rerank 模型(agentRequiresRerankModel)。

7.3 分享机制(agent_share)

internal/application/service/agent_share.go:Agent 可分享给组织(Organization)

  • 仅 Agent 属主租户可分享(ErrNotAgentOwner);分享者所在租户须为组织 Editor+ 成员;
  • 分享前校验 Agent 配置完整:必须有 model_id;若 knowledge_search 在其工具集内(或工具集为空回退默认集)且 KB scope 未禁用,还必须有 rerank_model_id,否则 ErrAgentNotConfigured
  • 权限强制为只读permission = types.OrgRoleViewer(跨租户编辑不在 v1 范围);重复分享则幂等更新;
  • 接收方租户可通过 TenantDisabledSharedAgentRepository 把某个共享 Agent 在本租户禁用;
  • 使用共享 Agent 对话时(session_agent_qa.go),检索与模型 scope 切到 Agent 属主租户resolveRetrievalTenantID),因此共享方的 KB 对使用方可用,而使用方自己的 MCP @提及会被限制在 Agent 预设内。

8. 内置 Agent(config/builtin_agents.yaml)

内置 Agent 由 config/builtin_agents.yaml 定义,启动时 types.LoadBuiltinAgentsConfig 载入并重建 BuiltinAgentRegistryinternal/types/builtin_agent_config.go),支持 default/zh-CN/zh-TW/ja-JP/ko-KR 多语言名称与描述;system_prompt_id/context_template_id 在启动时经 ResolveBuiltinAgentPromptRefs 解析为具体模板内容。

ID名称(zh-CN)agent_mode / agent_type关键配置
builtin-quick-answer快速问答quick-answer模板 default_kb + default_context;temperature 0.7;FAQ 优先(直接回答阈值 0.9、加权 1.2);query expansion + rewrite;web 搜索开、5 条;不进 Agent 引擎
builtin-smart-reasoning智能推理smart-reasoning / rag-qamax_iterations: 50;工具:knowledge_search、grep_chunks、list_knowledge_chunks、query_knowledge_graph、get_document_info;web 搜索开;多轮 5 轮
builtin-data-analyst数据分析师smart-reasoning / data-analysis模板 data_analyst;temperature 0.3;max_iterations: 30;工具仅 data_schema + data_analysis;限定 csv/xlsx;关闭 web 搜索;历史 10 轮
builtin-wiki-researcher维基问答smart-reasoning / wiki-qa模板 wiki_researchermax_iterations: 30;工具:wiki_search、wiki_read_page、wiki_read_source_doc、wiki_flag_issue(只读 + 报障);关闭 web 搜索
builtin-wiki-fixer维基修订smart-reasoning / custom模板 wiki_fixerretain_retrieval_history: true(修订需要跨轮记住页面内容);工具含全部 wiki 写操作(wiki_write_page、wiki_replace_text、wiki_rename_page、wiki_delete_page、wiki_read_issue、wiki_update_issue 等 9 个);kb_selection_mode: selected

补充两点(来自 internal/types/custom_agent.go):

  • builtin-wiki-fixer 有意不出现在用户可见的 Agent 列表(builtinAgentIDsOrdered 排除了它)——它是 Wiki 编辑器程序化调用的内部 Agent,但仍可经 GetAgentByID 使用;
  • builtinAgentIDsOrdered 中还保留了 builtin-deep-researcherbuiltin-knowledge-graph-expertbuiltin-document-assistant 等 ID 常量位次,但当前 YAML 未定义这些条目,注册表以 YAML 为准;
  • builtin_agents.yaml 里每个条目都带 reflection_enabled(数据分析师为 true,其余 false),但后端目前不消费这个字段——internal/ 下既没有对应的结构体字段也没有引用,只有 YAML 与前端类型定义里存在。也就是说它当前不影响 Agent 的实际行为,看到它为 true 不要以为多了一轮反思。

顺带一提,internal/agent/prompts_wiki.go 中的 WikiSummaryPromptWikiKnowledgeExtractPromptWikiTaxonomyPlanPrompt 等常量属于 Wiki ingest 管道(文档入库时 LLM 生成 wiki 页面/目录规划)使用的提示词,与 wiki 类 Agent 的运行时工具互补:前者生产 Wiki 内容,后者消费与维护。

9. Agent 模式与普通 RAG 问答模式

9.1 两条问答路径

路由层(internal/router/router.go)注册了两个入口:

go
knowledgeChat.POST("/:session_id", handler.KnowledgeQA)  // /knowledge-chat/:session_id
agentChat.POST("/:session_id", handler.AgentQA)          // /agent-chat/:session_id

两者最终都汇聚到 internal/handler/session/qa.go 的统一执行流 executeQA(reqCtx, mode, generateTitle)mode 二选一:

go
const (
	qaModeNormal qaMode = iota // KnowledgeQA pipeline (RAG / pure chat)
	qaModeAgent                // Agent engine with tool calling
)

9.2 模式决策逻辑

Handler.AgentQA 中的决策(真实代码逻辑):

  1. 解析请求并经 resolveAgent 解析 agent_id 对应的 CustomAgent(含内置与共享 Agent 的权限校验);
  2. CustomAgent.IsAgentMode() 优先于请求里的 agent_enabled 字段——即 Config.AgentMode == "smart-reasoning" 才走 Agent,quick-answer 型 Agent 即使打到 /agent-chat 也会被降级:
  3. 若 agent 模式成立但 customAgent == nil(典型场景:前端 localStorage 里 selectedAgentId 被清空但开关残留),提前返回 400 "agent_id is required when agent mode is enabled",避免异步流里报晦涩错误;
  4. 成立 → executeQA(reqCtx, qaModeAgent, true);否则打日志 "Agent mode disabled, delegating to normal mode" 并走 qaModeNormal

嵌入渠道(internal/handler/embed_channel.godelegateEmbedChat)同理:agentMode && ch.AgentID != types.BuiltinQuickAnswerID 才转发 AgentQA,否则 KnowledgeQA

9.3 两条路径的差异

维度普通 RAG(qaModeNormal)Agent(qaModeAgent)
执行体KnowledgeQA chat pipeline(意图识别→改写→检索→rerank→拼 context→单次生成)AgentEngine.Execute 的 ReAct 多轮循环
检索方式管道固定的向量/关键词混合检索LLM 自主选择工具(语义/正则/图谱/Wiki/Web/SQL…),可多轮迭代
服务入口sessionService.KnowledgeQAsessionService.AgentQA强制要求 req.CustomAgent != nil
历史管道自身的多轮改写与历史拼装LoadAgentHistory 重建 assistant+tool 消息级历史
结果持久化单条回答回答 + AgentSteps(思考/工具调用树),SSE 可回放
KB 兼容性隐式要求 vector 或 keyword 索引(quickAnswerKBFilterallowed_tools 的 capabilities 派生

sessionService.AgentQAinternal/application/service/session_agent_qa.go)在进入引擎前还处理:共享 Agent 的租户切换、视觉模型路由(模型支持 vision 则直传图片,否则把 VLM 描述并入 query)、引用上下文/附件内容并入 query、rerank 模型按需初始化等;执行是异步的,事件经 EventBus 流回 Handler 层。

10. 建议问题(Starters 与追问)

对话框在两个位置会给出可点击的问题:会话还空着时的开场问题(starters),以及每轮回答结束后的追问建议(follow-ups)。这套配置归 Agent 所有(QuestionSuggestionConfiginternal/types/custom_agent.go),渠道设置只能抑制展示,不能改内容策略。

配置项

两组配置各自独立开关,mode 决定问题从哪来:

mode来源
curated只用人工写死的 items
knowledge从知识库内容里取
generated让模型生成
hybrid(默认)上述几种混合
配置默认说明
starters.enabled / mode / items / count— / hybrid / 空 / 6开场问题
follow_ups.enabled / mode / count— / hybrid / 3追问建议
follow_ups.model_id空(用会话模型)生成追问用的模型,可指定小模型省成本
follow_ups.categories限定问题类型:clarify(澄清)/ deepen(深入)/ action(行动)
follow_ups.max_context_turns2生成时回看几轮对话
follow_ups.additional_instruction追加到生成提示词的业务约束
follow_ups.suppress_on_fallback回答走了兜底策略时不出建议
follow_ups.suppress_when_answer_asks_question回答本身在反问用户时不出建议(避免两个问题打架)
follow_ups.knowledge_fallback生成失败时回退到知识库来源
follow_ups.allow_regenerate是否允许用户手动换一批

生成、缓存与埋点

  • 结果存 message_suggestion_sets 表,按 (assistant_message_id, placement, config_hash, locale) 缓存——config_hash 把「当前生效的 Agent 配置」摘要进缓存键,所以改了配置会自然拿到新的一批,而不是读到旧缓存;locale 让多语言各自缓存;
  • 状态:generatingready,另有 suppressed(按上面的抑制规则跳过)与 failedlease_until 防止多实例重复生成同一批;
  • 接口:GET /sessions/:id/messages/:message_id/suggestions 读,POST 同路径触发生成(幂等),POST /sessions/:session_id/suggestion-events 上报埋点;
  • 埋点事件:impression(曝光)/ click(点击)/ dismiss(关掉)/ regenerate(换一批),存 message_suggestion_events。点击后发出的下一条用户消息会带 SuggestionAttributionsuggestion_set_id + question_id),因此统计上能区分「点了建议」与「自己打了同样的问题」。

11. 关键常量速查

常量位置
DefaultAgentMaxIterations20internal/agent/const.go
MAX_ITERATIONS(服务层上限)100internal/application/service/agent_service.go
defaultLLMCallTimeout120sinternal/agent/const.go
defaultToolExecTimeout60sinternal/agent/const.go
maxLLMRetries2internal/agent/const.go
maxEmptyResponseRetries2internal/agent/const.go
maxRepeatedResponseRounds2internal/agent/const.go
DefaultMaxToolOutput16000 rune(头 70% / 尾 30%)internal/agent/tools/truncate.go
DefaultMaxContextTokens200000internal/types/agent.go
DefaultConsolidationThreshold0.5internal/agent/memory/consolidator.go
DefaultContextThresholdRatio0.8internal/agent/token/compress.go
审批默认超时10 分钟internal/agent/approval/gate.go
沙箱默认限额60s / 256MB / 1 CPU / 100 pidsinternal/sandbox/sandbox.godocker.go
技能命名限制name ≤ 64、description ≤ 1024internal/agent/skills/skill.go