本文介绍自研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.complete → AgentGraphRunner(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_code 是 RiskLevel.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 投递。