AI基建之Agent运行时

本文介绍自研Agent套装N-Agent,一款类似Hermes的Agent Runtime。 核心流程:接收对话请求,加载会话上下文,循环”调用模型→按需执行工具”直至产出最终回答,更新会话与外部记忆,返回同步或流式结果。

领域划分

Agent Runtime
├── 核心子域
│   ├── Loop:运行状态推进、工具调用回环、结束判断
│   ├── AgentCore:模型调用、上下文组织、推理结果承接
│   ├── Memory:消息、会话、工具调用、任务状态、滚动摘要,以及外部记忆
│   ├── Action:把模型 tool_calls 转换为受控工具执行,工具由支撑子域实现
│   └── Policy:工具权限、风险等级、执行约束和安全决策
├── 支撑子域
│   ├── Skill:本地SKILL.md包管理,通过skills_list/skill_view暴露给 LLM 自助使用
│   ├── Knowledge:KB的SPI定义、实例管理,通过search_knowledge检索知识
│   ├── MCP:MCP site 注册与工具同步,把远程 MCP server 工具暴露给 LLM 调用
│   ├── Plugin:预留工具适配入口
│   ├── Platform/Gateway:飞书等外部消息平台抽象、生命周期管理;CLI/TUI 仅作为终端聊天入口来源
│   └── Sandbox:受控Python代码执行子域execute_code
└── 通用子域
    └── Environment:模型、存储、文件、网络等外部资源边界

概念架构

  • Skill:结构化Prompt,控制LLM怎么想、怎么说,进而完成功能。Skill定义业务功能主决策而非执行,这是和工具的主要区别
  • Plugin:特化工具为LLM定制的点对点适配,将外部工具能力、封装为LLM可调用函数FC。Plugin是本地部署的工具适配层,而非工具本身
  • MCP:面向LLM的标准协议,用于统一外部工具、资源、上下文的访问方式,类似总线协议、SPI
概念架构图
%% align: left
graph TD

Skill --> AgentCore
AgentCore --> Knowledge
AgentCore --> MCP
AgentCore --> Plugin

Knowledge --> FC
MCP --> FC
Plugin --> FC

FC --> externals

Loop

User Message
    ↓
AgentCore
    ↓
Tool Calls?
    ├─ Yes → Execute Tool → Observation → AgentCore
    └─ No  → Final Answer
Loop FSM
stateDiagram-v2
  [*] --> load_context
  load_context --> call_llm
  call_llm --> execute_tools: pending_tool_calls
  call_llm --> update_memory: no tool calls
  call_llm --> finalize: error
  execute_tools --> update_memory
  update_memory --> call_llm: continue
  update_memory --> finalize: error / final_message / iteration_limit reached
  finalize --> [*]

库:LangGraph.Graph.StateGraph

AgentCore

AgentCore: Single-Turn Inference Engine(单轮推理引擎)

Input
├── Runtime State
│     ├── History
│     ├── Current Input
│     ├── Summary
│     ├── Memory
│     └── Optional Retrieved Context
├── Runtime Capabilities
│     └── Tool Registry
├── Runtime Constraints
│     └── Policies
└── Model Configuration

        ↓

AgentCore

        ↓

Output
├── Assistant Message
├── Tool Calls
├── Finish Reason
├── Usage
└── Reasoning Metadata
AgentCore详情
AgentCore
│
├── Context Assembly: produces Message Context
│     ├── History
│     ├── Current Input
│     ├── Summary
│     ├── Memory
│     └── Optional Retrieved Context
│
├── Prompt Builder: produces System Context
│     ├── Identity
│     ├── Instruction
│     ├── Safety / Policy Guidance
│     ├── Platform Guidance
│     └── Prompt Composition
│
├── Tool Assembly: produces Tool Context
│     ├── Tool Selection
│     ├── Authorization
│     ├── Schema Normalization
│     └── Tool Choice
│
├── Request Normalization: produces Provider-Agnostic Inference Request
│     ├── System Context
│     ├── Message Context
│     ├── Tool Context
│     └── Generation Parameters
│
├── Provider Adapter
│     ├── adapts to Provider API Request
│     ├── invokes Provider
│     └── receives Raw Provider Response
│
├── Response Normalization: produces Normalized Inference Result
│     ├── Content
│     ├── Tool Calls
│     ├── Finish Reason
│     ├── Usage
│     ├── Reasoning Metadata
│     └── Raw Response
│
└── Inference Handoff: produces Agent Runtime Output
      ├── Assistant Message
      ├── Tool Calls
      ├── Finish Reason
      ├── Usage
      └── Reasoning Metadata

Memory

Memory 子域负责让 Agent 在单轮推理之外保留上下文。按生命周期,记忆分类如下:

Memory
├─ 会话记忆(SQLite持久化,session内有效)
│  ├─ 会话 ConversationSession
│  ├─ 消息 ConversationMessage
│  ├─ 工具调用 ToolCall
│  ├─ 任务状态 TaskState
│  └─ 滚动摘要 Summary
└─ 外部记忆(跨session持久,按载体分三类)
   ├─ 系统记忆(builtin):根目录下的 {memory,user,observations}.md、memory.meta.json
   │    ├─ memory.md:稳定知识
   │    ├─ user.md:用户偏好
   │    ├─ observations.md:sync_turn 自动抽取的轮次关键词
   │    └─ memory.meta.json:memory.md 条目的信任度/时间衰减元数据
   ├─ 文件记忆(multi-project):按项目目录拆分的多组 {memory,user}.md
   └─ 检索记忆(external-query):通过query走向量/语义检索拿回相关片段,互斥、全局至多1个active provider
        ├─ mem0:服务端事实库(HTTP)
        ├─ holographic:本地 SQLite + MemoryRetriever(HRR 向量库)
        └─ honcho:用户建模 dialectic 库(HTTP)
Context Frame: Memory In Context

完整上下文可以理解为一次模型调用前组装出的 Context Frame,从稳定上下文、到本轮临时上下文逐层合成。Memory 不是单独追加的一段文本,而是根据特征进入稳定层、会话层、本轮层。最终发给模型的是 Provider Request(provider-agnostic request),返回后再拆成 assistant message、tool calls、usage 和推理元数据。

Context Frame(上下文来源分层)
├─ 1. Stable Context
│  ├─ System Context(静态常量,平台无关;多项拼为单条 system message)
│  │  ├─ identity: DEFAULT_AGENT_IDENTITY
│  │  ├─ instruction: REACT_GUIDANCE + KNOWLEDGE_GUIDANCE + MANAGED_TOOL_GUIDANCE
│  │  ├─ safety: SAFETY_GUIDANCE
│  │  └─ *外部记忆*静态快照:系统记忆(builtin)/文件记忆(multi-project)的 {memory,user}.md
│  └─ Capability Context
│     └─ tool definitions: name + description + parameters
│
├─ 2. Session Context(*会话记忆*)
│  ├─ 消息 ConversationMessage: 历史消息,包括 user / assistant / tool
│  ├─ 会话 ConversationSession: id + title + source + external_memory_enabled(持久化但不进入Provider Request)
│  ├─ 工具调用 ToolCall: 工具执行记录(持久化但不进入Provider Request,工具结果已包含在role=tool的消息中)
│  ├─ 任务状态 TaskState: 任务运行状态(持久化但不进入Provider Request)
│  └─ 滚动摘要 Summary(持久化但不进入Provider Request)
│
├─ 3. Turn Context
│  ├─ input: latest user messages
│  ├─ *外部记忆*动态检索(retrieved memory): prefetch_all → <memory-context> 围栏,prepend 到 last user message content 副本
│  └─ run options: external_memory_enabled + tool_exposure_policy + tool_execution_context + execution_context_mode(控制信号,不进入Provider Request)
│
└─ 4. Execution Context(执行现场,不进入Provider Request)
   ├─ tool filtering: safe_only / default
   ├─ ToolExecutionContext: 工具授权 + execution_context_mode + enabled_override + trusted_metadata
   ├─ agent_context(trusted_metadata 内): primary / subagent / cron / unattended(控制外部记忆写入权限)
   └─ 运行进度: AgentState.run_status / iteration_count / error(控制 finalize)

       │ 组装
       ▼

Provider Request
├─ messages  ← 1.System Context + 2.消息ConversationMessage + 3.input&retrieved memory
├─ tools     ← 1.Capability Context(经 4.tool filtering 过滤)
├─ tool choice: 默认
└─ generation params: temperature / top_p / top_k / max_tokens / stop_sequences / thinking / cache_control 等
真实对话示例
#### 对话
>> User:现在几点了?打印下UTC时间。
>> Tool:
{
  "tool_call_id": "call_u8usl364ddpffrw3q5758dk3",
  "name": "get_current_time",
  "status": "success",
  "content": {
    "now": "2026-06-27T15:52:06.344631+00:00"
  },
  "duration_ms": 0
}
>> Assistant:外部记忆1:当前的UTC时间是2026年06月27日 15:52:06.344631。


#### Context Frame

会话事实:source=dashboard(primary),external_memory_enabled=["builtin","external_memory_1"],
builtin/memory.md 为空,external_memory_1/memory.md = `所有的回复,必须以"外部记忆1:"开头儿。`。
共 2 次 LLM 推理(iteration_count=2):第 1 次产出 tool_calls,第 2 次产出 final。

=== 第 1 次推理 ===
1. Stable Context
   ├─ System Context
   │  ├─ identity / instruction / safety(静态常量)
   │  └─ *外部记忆*静态快照:
   │     ├─ builtin/memory.md → ""(空文件不产生 block,跳过)
   │     └─ external_memory_1/memory.md → "所有的回复,必须以"外部记忆1:"开头儿。"
   └─ Capability Context: tool definitions(含 get_current_time)
2. Session Context(首轮 history 为空)
3. Turn Context
   ├─ input: "现在几点了?打印下UTC时间。"
   └─ *外部记忆*动态检索: ""(系统记忆/文件记忆 均用 MemoryRetriever 按 query 检索 memory.md entry;本例 query 与 external_memory_1 的 entry 无词重叠,返回空)
4. Execution Context: agent_context=primary,tool filtering=default
   │ 组装
   ▼
Provider Request #1
├─ messages: [system(含外部记忆静态快照), user("现在几点了?...")]
└─ tools: [get_current_time, ...]
→ LLM 返回: tool_calls=[get_current_time(id=call_u8us...)]

--- 中间落盘(execute_tools + update_memory #1)---
ToolCall 持久化: call_u8us..., get_current_time, success, result={now:2026-06-27T15:52:06.344631+00:00}
消息 ConversationMessage:
  ├─ append(role=assistant, content="", tool_calls=[...])
  └─ append(role=tool, tool_call_id=call_u8us..., content=result)  ← 工具结果经 role=tool 消息回流
TaskState: status=running, iteration_count=1
Summary: HeuristicSummarizer 覆盖(未达阈值,仍持久化;不进 Provider Request)
*外部记忆*自动更新: 本轮 LLM 未调用 external_memory 工具 → 文件未变

=== 第 2 次推理 ===
1. Stable Context(同 #1,外部记忆静态快照再次注入)
2. Session Context(首轮历史已落盘)
   ├─ 消息 ConversationMessage history:
   │  ├─ user("现在几点了?打印下UTC时间。")
   │  ├─ assistant(content="", tool_calls=[get_current_time])
   │  └─ tool(content={now:...})  ← 工具结果通过 role=tool 消息进入 history
   ├─ 工具调用 ToolCall: 已持久化(不进请求,结果已在 role=tool 消息中)
   ├─ 任务状态 TaskState: running(不进请求)
   └─ 滚动摘要 Summary: 已持久化(不进请求,仅作下次 summarize 入参)
3. Turn Context
   ├─ input: (无新 user 输入,本轮由 tool 结果驱动)
   └─ *外部记忆*动态检索: ""
4. Execution Context: iteration_count=1→2
   │ 组装
   ▼
Provider Request #2
├─ messages: [system(含外部记忆静态快照), user, assistant(tool_calls), tool(result)]
└─ tools: [get_current_time, ...]
→ LLM 返回: final="外部记忆1:当前的UTC时间是2026年06月27日 15:52:06.344631。"
   ↑ 前缀严格遵循 external_memory_1 静态快照指令 —— 证明外部记忆经 Stable Context 生效

--- finalize ---
消息 ConversationMessage: append(role=assistant, final)  ← scrub_memory_context 剥离 <memory-context>(本轮无此标签)
TaskState: status=completed, iteration_count=2
Summary: 覆盖为 "用户: 现在几点了?... | 助手: 外部记忆1:..."
*外部记忆*消息同步: sync_all(primary) → 系统记忆(builtin).sync_turn 抽取关键词写入 observations.md;external_memory_1(文件记忆).sync_turn 为 no-op
单轮写入时序

ChatCompletionService.completeAgentGraphRunner(LangGraph):

complete
  ├─ 会话 ConversationSession: create_session (INSERT OR IGNORE)
  ├─ 会话 ConversationSession.external_memory_enabled: lock_session_external_memory(首写获胜)
  ├─ 消息 ConversationMessage: append_message(role=user) × N
  └─ 会话 ConversationSession.title: ensure_title(异步后台)
load_context(装配 working_messages:system + history + input)
  ├─ 消息 ConversationMessage: list_messages → 历史消息 history
  ├─ 滚动摘要 Summary: get_summary → state.summary(不进 messages,仅作下次 summarize 入参)
  └─ *外部记忆*静态快照: 随 system prompt 注入(Stable Context)
  ├─ *外部记忆*动态检索: prefetch_all → <memory-context> prepend 到 last user message 副本(不污染 state)
  ├─ llm.chat → Provider Request
  └─ scrub_memory_context(final_message.content)  # 立即剥离回声,防写回 消息 ConversationMessage
execute_tools
  ├─ 工具调用 ToolCall: save_tool_call 持久化工具执行事实
  └─ *外部记忆*自动更新: LLM 主动调用工具external_memory,更新外部记忆 Markdown 文件
update_memory(更新*会话记忆*)
  ├─ 消息 ConversationMessage: append_message(role=assistant, content 含 tool_calls)
  ├─ 消息 ConversationMessage: append_message(role=tool, tool_call_id, name)
  ├─ 任务状态 TaskState: save_task_state(status=running|failed)
  └─ 滚动摘要 Summary: summarize → save_summary(覆盖最新值)
       └─ *外部记忆*压缩前抢救: pre_compress_all → provider.on_pre_compress 返回要点,回填到 summary
finalize
  ├─ 消息 ConversationMessage: 错误兜底 append_message(role=assistant, 错误文案)
  ├─ *外部记忆*消息同步: sync_all,同步本轮对话内容、给外部记忆provider,系统记忆(builtin)写 observations.md,文件记忆(multi-project)/检索记忆(external-query)为 no-op
  └─ 任务状态 TaskState: save_task_state(status=run_status)

关键边界:会话记忆由 MemoryStore 屏蔽 SQLite;外部记忆由 ExternalMemoryManager 路由到 provider。写入路径有两条:LLM 主动调用 external_memory / multi_external_memory 工具(execute_tools),以及 finalize 阶段 sync_all(builtin 写 observations.md,multi-project / external-query 为 no-op)。LLM 回声中的 <memory-context> 在 call_llm 内立即剥离,避免写回 ConversationMessage。

Action

Action 子域把 LLM tool_calls 转换为受控工具执行。所有工具共享公共链路,在 ToolExecutor执行器 层按工具来源分化。

工具执行

工具执行的公共链路,如下,

LLM tool_calls
  -> AgentGraphRunner.execute_tools
  -> ToolCallRequest(id, name, arguments)
  -> ToolService.execute
       ├─ lookup ToolDefinition
       ├─ Policy 判定(enabled / risk_level / managed / permitted_managed_tools)
       └─ CompositeToolExecutor.execute
            └─ routes[name] -> ToolExecutor, 不同Tool在执行器上分化
  -> ToolResult(tool_call_id, tool_name, status, content)
  -> AgentGraphRunner写 role=tool 消息,回流给LLM
  -> LLM 接收 tool result,继续推理
  • ToolDefinition 统一工具描述,包括 name/description/parameters/risk_level/source_type
  • ToolResult 统一工具调用结果
  • CompositeToolExecutor.routes将不同类型tool、路由到不同的执行器ToolExecutor
工具执行器分化

工具执行器ToolExecutor按工具来源分化,主要实现包括Builtin/Knowledge/Mcp/Plugin/Skill等。核心执行器如下:

Knowledge
  KnowledgeToolExecutor -> KnowledgeService.search(kb_id, query)
    ├─ registry.get(kb_id) + get_secret(kb_id)
    ├─ retriever_factory.create(config, secret)
    └─ retriever.search -> normalize -> KnowledgeSearchResult

MCP
  McpToolExecutor(兼 CompositeToolExecutor.fallback)
    -> McpService.call_tool(site_id, remote_name, args)
    -> McpClient(SDK) -> 远端 MCP server(streamable_http / SSE / stdio)

Plugin
  PluginToolExecutor -> PluginService.call_tool(name, args, context, tool_call_id)
    ├─ lookup PluginToolRegistration + check_fn
    ├─ 注入 plugin_config / secret_config / session_id / metadata 为 kwargs
    └─ handler(args, **kwargs) -> dict 包装

Skill
  SkillToolExecutor -> SkillService.list_skills / render_view(name)
    └─ 读 SKILL.md frontmatter+body

工具注入

ToolService.definitions         <- 静态内置工具(BUILTIN)
ToolService.dynamic_definitions <- 动态工具面,按 source_type 分槽
  ├─ "skill"     <- SkillService(skills_list / skill_view)
  ├─ "knowledge" <- KnowledgeService(search_knowledge)
  ├─ "mcp"       <- McpService(site probe 后注入远端工具)
  └─ "plugin"    <- PluginService(register(ctx) 后注入 plugin 工具)

Sandbox

Sandbox 子域承载 execute_code:为模型生成的 Python 代码提供隔离执行、受控文件/网络能力和独立审计。后端支持 Docker 与 Local,生产语义以 Docker 隔离为准。

模型调用 execute_code 的执行链路如下,所有入口统一走 ToolService:

Interface/Gateway -> ChatCompletionService -> AgentGraphRunner.execute_tools
  -> ToolService.execute -> SandboxToolExecutor.execute
       ├─ 会话锁
       ├─ get_or_create(session_id)       # session 级复用
       ├─ new_call_staging(session_id)    # per-call scratch
       └─ sandbox.execute(request)
  -> record_history
  -> ToolResult 回流 AgentGraph,写 role=tool 消息

safe_only 策略隐藏 ToolSourceType.AGENT 来源工具,unattended/scheduler 默认不暴露 execute_code;交互通道不受影响。

Dashboard API/CLI 的直接执行不经模型 tool_calls,但复用 SandboxToolExecutor 和同一沙盒边界。

生命周期

Sandbox 生命周期如下。release 由 idle 到期或 session 删除触发;manual force 由 Dashboard 手动触发(docker kill -f)。

stateDiagram-v2
  [*] --> active: get_or_create
  active --> executing: execute
  executing --> active: done
  active --> releasing: release
  executing --> releasing: force
  active --> releasing: force
  releasing --> [*]: cleanup
业务关系
erDiagram
  ConversationSession ||--o| Sandbox : "1:1 session级复用"
  Sandbox ||--o{ SandboxExecutionHistoryEntry : "1:N 每次执行一条审计"
  Sandbox ||--o| ReleasedSandboxInfo : "1:0..1 释放时一条"
安全边界

execute_codeRiskLevel.SAFE 工具,无 confirmation gate,不依赖 trusted_metadata。安全边界在 sandbox 内部:

  • 隔离:workspace 只读,scratch 可写;Docker 后端禁网络、降权、限制进程和临时目录。
  • 外部能力:代码只能通过注入的 UDS RPC stub 调 callback tool;父进程按 allowlist 与 max_tool_calls 派发。
  • 审计:每次执行写入 SandboxExecutionHistoryRegistry,含 code_hash、状态、结果和授权工具。
  • 失败语义:sandbox 异常转成 ToolResult(ERROR),不打断 AgentGraph。

用户接口

N-Agent 入口类型,有如下几类:

入口 传输+编码协议 适配器 应用层 适配器源文件
Dashboard 管理 API HTTP+JSON create__router / register__routes 不进入 Agent Runtime app/interfaces/http/
OpenAI 兼容对话 API HTTP/SSE+JSON create_openai_compatible_router ChatCompletionService app/interfaces/http/openai_compatible.py
飞书 IM 长连接 WebSocket+JSON FeishuImAdapter GatewayService → ChatCompletionService app/interfaces/feishu_im_adapter.py
TUI/CLI Chat Stdio+行式文本 CliChatAdapter GatewayService → ChatCompletionService app/interfaces/cli/
ACP Agent Stdio+JSON NAgentACPAgent GatewayService → ChatCompletionService app/interfaces/cli/commands/acp/
定时任务执行 - SchedulerRunner ScheduleRunService → ChatCompletionService app/application/scheduler_runner.py

其中,

  • 管理API不进入 Agent Runtime 应用层;
  • OpenAI 兼容对话 API 直接进入 ChatCompletionService;
  • 飞书 IM、TUI/CLI、ACP 的用户消息先经 GatewayService 统一做入口会话、消息管理,再进入 ChatCompletionService。
  • ACP协议生命周期保留在 NAgentACPAgent 中。
  • 定时任务执行由 SchedulerRunner 定时触发,并通过 ScheduleRunService->ScheduledAgentExecutor 直接调用 ChatCompletionService,执行结果再由 ScheduleOutboundDelivery 投递。