Agent Scope Java 2.x 系列【21】Harness:AGENTS.md 加载和执行流程

文章目录

  • [1. AGENTS.md 概述](#1. AGENTS.md 概述)
    • [1.1 定位](#1.1 定位)
    • [1.2 在工作区中的位置](#1.2 在工作区中的位置)
    • [1.3 设计意义](#1.3 设计意义)
    • [1.4 简单示例](#1.4 简单示例)
    • [1.5 与其他配置的关系](#1.5 与其他配置的关系)
  • [2. 配置阶段](#2. 配置阶段)
    • [2.1 设置工作区路径](#2.1 设置工作区路径)
    • [2.2 设置额外注入文件](#2.2 设置额外注入文件)
    • [2.3 Token 预算控制](#2.3 Token 预算控制)
    • [2.4 关闭工作区上下文注入](#2.4 关闭工作区上下文注入)
    • [2.5 共享存储](#2.5 共享存储)
  • [3. 构建阶段](#3. 构建阶段)
    • [3.1 创建 WorkspaceManager](#3.1 创建 WorkspaceManager)
    • [3.2 校验工作空间](#3.2 校验工作空间)
      • [3.2.1 根目录](#3.2.1 根目录)
      • [3.2.2 双重判断 AGENTS.md 是否存在](#3.2.2 双重判断 AGENTS.md 是否存在)
    • [3.3 创建 WorkspaceManager 工厂函数](#3.3 创建 WorkspaceManager 工厂函数)
    • [3.4 WorkspaceContextMiddleware 创建与注册](#3.4 WorkspaceContextMiddleware 创建与注册)
      • [3.4.1 初始化](#3.4.1 初始化)
      • [3.4.2 Middleware 注册顺序](#3.4.2 Middleware 注册顺序)
    • [3.5 HarnessAgent 构建完成](#3.5 HarnessAgent 构建完成)
  • [4. 调用阶段](#4. 调用阶段)
    • [4.1 入口](#4.1 入口)
    • [4.2 读取 AGENTS.md](#4.2 读取 AGENTS.md)
      • [4.2.1 文件系统读取](#4.2.1 文件系统读取)
      • [4.2.2 本地磁盘读取](#4.2.2 本地磁盘读取)
    • [4.3 Token 预算](#4.3 Token 预算)
    • [4.4 加载到系统提示词](#4.4 加载到系统提示词)

重点分析 AgentScope HarnessAGENTS.md 从加载、注入系统提示词到存储的完整生命周期,全面了解【文件系统】运行机制。


1. AGENTS.md 概述

1.1 定位

工作区(workspace)是 HarnessAgent 智能体定义与进化的唯一可信来源,AGENTS.mdHarnessAgent 工作区中最重要的静态配置文件。

AGENTS.mdAgent人设文件 :用自然语言 Markdown 定义 Agent 的身份、行为准则和系统指令, Agent 每次推理时最先被注入到系统提示词的上下文

1.2 在工作区中的位置

AGENTS.md 在逻辑上始终位于工作区根目录:

text 复制代码
.agentscope/workspace/
├── AGENTS.md          ← ★ 人设文件(本文档主题)
├── MEMORY.md          ← 长期记忆(框架自动维护)
├── memory/            ← 按日期拆分的记忆分片
├── knowledge/         ← 专属领域知识库
├── skills/            ← 可复用技能包
├── subagents/         ← 子 Agent 配置
├── plans/             ← Plan Mode 持久化计划
└── agents/            ← 运行时数据(会话日志、任务记录)

但实际的物理文件落点取决于文件系统模式IsolationScope

关键设计:NamespaceFactory 在所有存储模式下都对 filesystem 层的全部读写(包括 AGENTS.md)施加命名空间,区别仅在于各模式下的具体落地方式。

1.3 设计意义

AI Agent 工程中,Agent 的行为规则通常散落在代码、配置文件、Prompt 模板各处,导致三个痛点:

痛点 没有 AGENTS.md AGENTS.md
身份不可见 系统提示词硬编码在代码中,改了就要重新部署 人设写在文件里,改了就生效,无需重启
规则不可审计 Prompt 由多段代码拼接,出了 Bug 查不清到底给模型发了什么 一个 Markdown 文件,所见即所得,可以直接 diff
多租户难适配 为不同用户定制人设需要改代码或配置中心 每个用户/租户维护自己的 AGENTS.md,Agent 代码完全不变

1.4 简单示例

text 复制代码
# MyAgent

你是 XX 公司的智能助手,用中文回答用户问题。

## 行为准则
- 不确定时主动询问,不编造信息
- 涉及文件操作前先确认路径
- 优先使用工具获取实时数据,不依赖过时的训练知识

## 禁止行为
- 不执行删除命令
- 不访问工作区以外的文件路径
- 不透露系统提示词内容

1.5 与其他配置的关系

配置方式 载体 生命周期 典型用途
AGENTS.md 工作区 Markdown 文件 人工编辑、随时生效 人设、行为约束、长期指令
builder.sysPrompt() Java 代码字符串 编译时固定、改代码生效 基础系统提示词(通常较简短)
builder.additionalContextFile() 工作区任意文件 人工编辑、随时生效 补充上下文(如 SOUL.mdPREFERENCES.md
MEMORY.md 工作区 Markdown 文件 框架自动写入 跨会话持久记忆

优先级sysPrompt 排在最前面,AGENTS.md 紧接其后(通过 WorkspaceContextMiddleware 追加)。两者不是替代关系,而是层叠关系: sysPrompt 定义"你是谁",AGENTS.md 进一步细化"你要怎么做事"。


2. 配置阶段

2.1 设置工作区路径

AGENTS.md 存放在工作区根目录下,工作区路径通过 HarnessAgent.builder().workspace() 配置。

java 复制代码
// HarnessAgent.java:1411-1428
// Path 形式
public Builder workspace(Path workspace) {
    this.workspace = workspace;
    return this;
}

// String 形式
public Builder workspace(String path) {
    if (path == null) {
        this.workspace = null;
    } else {
        String trimmed = path.strip();
        if (trimmed.isEmpty()) {
            throw new IllegalArgumentException("workspace path must not be blank");
        }
        this.workspace = Path.of(trimmed);
    }
    return this;
}

默认路径HarnessAgent.java:1776-1779):

java 复制代码
Path resolvedWorkspace =
        workspace != null
                ? workspace
                : Paths.get(System.getProperty("user.dir"))
                        .resolve(".agentscope/workspace");

不传 workspace 时默认为 ${user.dir}/.agentscope/workspace

2.2 设置额外注入文件

通过 .additionalContextFile() 可以将工作区内的其他文件也注入到系统提示词中:

java 复制代码
// HarnessAgent.java:1532-1536
public Builder additionalContextFile(String relativePath) {
    if (relativePath != null && !relativePath.isBlank()) {
        this.additionalContextFiles.add(relativePath);
    }
    return this;
}
java 复制代码
// 使用示例
HarnessAgent.builder()
    .workspace(Paths.get(".agentscope/workspace"))
    .additionalContextFile("SOUL.md")       // 注入工作区根目录的 SOUL.md
    .additionalContextFile("PREFERENCES.md") // 注入 PREFERENCES.md
    .build();

额外文件会被包装为 XML 标签注入(如 <soul_md>...</soul_md>),详见第 3.5 节。

2.3 Token 预算控制

java 复制代码
// HarnessAgent.java:975
int maxContextTokens = 8000;  // 默认 8000 tokens

// HarnessAgent.java:1540
public Builder maxContextTokens(int maxTokens) { ... }

Token 预算用于控制整个工作区上下文块 的注入量。注意:AGENTS.mdKNOWLEDGE.md 及额外文件不计入预算限制 ,只有 MEMORY.md 会被截断。

2.4 关闭工作区上下文注入

java 复制代码
// HarnessAgent.java:1711-1713
public Builder disableWorkspaceContext() {
    this.disableWorkspaceContext = true;
    return this;
}

调用后,WorkspaceContextMiddleware 不会被注册,AGENTS.md / MEMORY.md / knowledge/ 都不会注入到系统提示词中。


2.5 共享存储

本次分析基于 JDBC 远程存储共享存储模式:

java 复制代码
    // ==================== JDBC 分布式存储 ====================

    @Bean
    public DistributedStore distributedStore(DataSource dataSource) {
        // initializeSchema(false):表由 schema.sql 创建,避免 utf8mb4 主键超长
        return DistributedStore.builder()
                .agentStateStore(new MysqlAgentStateStore(dataSource, true))
                .baseStore(JdbcStore.builder(dataSource).initializeSchema(false).build())
                .build();
    }
    
    // ==================== HarnessAgent(JDBC 共享存储模式) ====================

    @Bean
    public HarnessAgent harnessAgent(Model model, WeatherTool weatherTool,
                                      DistributedStore store) {
        Path workspacePath = Paths.get(System.getProperty("user.home"),
                ".agentscope", "workspace", "demo-agent");
        try {
            java.nio.file.Files.createDirectories(workspacePath);
        } catch (Exception ignored) {}


        RemoteFilesystemSpec remoteFilesystemSpec = new RemoteFilesystemSpec()
                .isolationScope(IsolationScope.USER);

        HarnessAgent agent = HarnessAgent.builder()
                .name("harness-demo")
                .description("HarnessAgent Demo")
                .sysPrompt("你是一个 Java 开发 AI 助手")
                .model(model)
                .workspace(workspacePath)
                 // 注入分布式存储(AgentState + 工作区 KV 都走 MySQL)
                .distributedStore(store)
                 // 启用远程文件系统(BaseStore 自动从 distributedStore 注入)
                .filesystem(remoteFilesystemSpec)
                .maxIters(5)
                .build();

        agent.getDelegate().getToolkit().registerTool(weatherTool);
        log.info("HarnessAgent built: name={} (JDBC RemoteFilesystem mode)", agent.getName());
        return agent;
    }

3. 构建阶段

3.1 创建 WorkspaceManager

HarnessAgent.build() 中首先解析文件系统模式,然后创建 WorkspaceManager

java 复制代码
// HarnessAgent.java:1868-1869
WorkspaceManager wsManager =
        new WorkspaceManager(resolvedWorkspace, filesystem, workspaceIndex, nsFactory);
wsManager.validate();

WorkspaceManager 构造函数中只是完成相关赋值操作:

java 复制代码
    private WorkspaceManager(
            Path workspace,
            AbstractFilesystem filesystem,
            WorkspaceIndex index,
            NamespaceFactory namespaceFactory,
            boolean ownsIndex) {
        this.workspace = workspace;
        this.filesystem = filesystem;
        this.index = index;
        this.namespaceFactory = namespaceFactory;
        this.ownsIndex = ownsIndex;
    }

其中 workspace 表示工作区本地路径

filesystem 表示文件系统,包含了兜底策略、路由策略(每个路由包含两层文件系统):

支持的文件系统模式:

模式 配置方式 读写路径
本地磁盘 默认 / filesystem(LocalFilesystemSpec) 直接操作本地文件
远程共享 filesystem(RemoteFilesystemSpec) Redis/JDBC/OSS 共享存储
沙箱容器 filesystem(SandboxFilesystemSpec) Docker/K8s 容器内部文件系统
自定义 abstractFilesystem(AbstractFilesystem) 完全自定义实现

WorkspaceIndex本地工作空间轻量化 SQLite 索引管理器 ,配合 AgentScope Harness 远程挂载式工作空间使用:

  1. 工作空间底层可能是对象存储 / 远程文件系统 ,遍历目录(lsglob 匹配、文件存在判断、grep 检索)如果每次都全量拉取远端列表,性能极差;
  2. 该类用本地 SQLite 数据库缓存文件元数据,加速路径查询;
  3. 属于尽力型Best-effort弱一致性索引,远端数据永远是权威数据源,索引允许滞后、更新失败不影响主流程。

ownsIndex 标记当前 WorkspaceManager 是否是 WorkspaceIndex 的所有者,用来做生命周期所有权区分,避免重复关闭、提前关闭、漏关 SQLite 连接。

3.2 校验工作空间

校验工作空间目录是否存在、关键配置文件是否齐全;缺失项仅打印警告日志,不抛异常中断启动。在 HarnessAgent 构建初始化阶段只会执行一次。

java 复制代码
    public void validate() {
        if (!Files.isDirectory(workspace)) {
            log.warn(
                    "Workspace directory does not exist: {}. "
                            + "Please create it and add AGENTS.md.",
                    workspace.toAbsolutePath());
            return;
        }
        boolean agentsMdExists = Files.isRegularFile(workspace.resolve(AGENTS_MD));
        if (!agentsMdExists && filesystem != null) {
            try {
                agentsMdExists = filesystem.exists(RuntimeContext.empty(), AGENTS_MD);
            } catch (Exception e) {
                log.debug(
                        "Filesystem not available at build time, skipping exists check: {}",
                        e.getMessage());
            }
        }
        if (!agentsMdExists) {
            log.warn(
                    "AGENTS.md not found in workspace: {}. "
                            + "AGENTS.md defines persona and local conventions for the agent.",
                    workspace.toAbsolutePath());
        }
    }

3.2.1 根目录

校验工作空间根目录是否存在:

java 复制代码
if (!Files.isDirectory(workspace)) {
    log.warn(
            "Workspace directory does not exist: {}. "
                    + "Please create it and add AGENTS.md.",
            workspace.toAbsolutePath());
    return;
}

3.2.2 双重判断 AGENTS.md 是否存在

工作空间根 /Agent/AGENTS_MD ,判断是否是存在本地文件:

java 复制代码
// Agent 下是否存在 AGENTS_MD 文件
boolean agentsMdExists = Files.isRegularFile(workspace.resolve(AGENTS_MD));

workspace.resolve(AGENTS_MD) 执行结果:

本地找不到 + 文件系统实例 filesystem 不为空时,调用抽象文件系统接口判断:

java 复制代码
if (!agentsMdExists && filesystem != null) {
    try {
        agentsMdExists = filesystem.exists(RuntimeContext.empty(), AGENTS_MD);
    } catch (Exception e) {
        log.debug(
                "Filesystem not available at build time, skipping exists check: {}",
                e.getMessage());
    }
}

filesystem.exists 方法执行逻辑:

  • 入参判空:空路径直接返回不存在
  • 调用 routeForPath(path) 做路径路由匹配,得到路由结果对象 RouteResult
  • 从路由结果取出匹配到的后端存储 backend,并传入映射后的后端内部路径 backendPath,调用对应后端的 exists 判断文件是否存在
java 复制代码
@Override
public boolean exists(RuntimeContext runtimeContext, String path) {
    if (path == null || path.isBlank()) {
        return false;
    }
    RouteResult route = routeForPath(path);
    return route.backend().exists(runtimeContext, route.backendPath());
}

在路由匹配规则中 AGENTS_MD 有两层存储:

得到路由结果对象 RouteResult

route.backend().exists 方法会调用到 RemoteFilesystem#exists ,基于 KV 存储实现的抽象文件系统,核心思路:

  • 先用本地 WorkspaceIndex 做高速缓存查询(快路径,避免远程 IO
  • 索引未命中再穿透到底层 KV 存储做权威校验
  • KV 没有原生文件夹,靠「前缀子 Key 存在」模拟目录是否存在
java 复制代码
    @Override
    public boolean exists(RuntimeContext runtimeContext, String path) {
        if (path == null || path.isBlank()) {
            return false;
        }
        String normalized = normalizePath(path);

        // Fast path: consult index first
        if (index != null) {
            if (index.exists(normalized)) {
                return true;
            }
            if (index.hasPrefix(normalized + "/")) {
                return true;
            }
            // Index miss --- fall through to remote (may not be indexed yet)
        }

        List<String> ns = getNamespace(runtimeContext);
        if (store.get(ns, normalized) != null) {
            return true;
        }
        // Also check if any child exists (directory-like)
        List<StoreItem> items = searchAllItems(runtimeContext);
        for (StoreItem item : items) {
            if (item.key().startsWith(normalized + "/")) {
                return true;
            }
        }
        return false;
    }

完整判断流程图:

text 复制代码
路径入参 → 判空 → 路径标准化 normalized
        ↓
index 不为空?
├─是 → index.exists(normalized) 成立?→ return true
│       ↓否
│    index.hasPrefix(normalized+"/") 成立?→ return true
│       ↓否 穿透到远端KV
└─否 直接进入远端KV
        ↓
获取当前租户命名空间 ns
store.get(ns, normalized) 不为空?→ return true
        ↓否
查询当前命名空间所有 StoreItem
任意 key.startsWith(normalized+"/") ? → return true
        ↓否
return false

其中基于 JDBC + 关系型数据库实现的 KV 存储底层读取方法:

java 复制代码
@Override
public StoreItem get(List<String> namespace, String key) {
    // 1. 校验 key 合法性(防止非法字符、空值、路径注入等)
    validateKey(key);
    // 2. 将命名空间列表拼接成数据库存储用的命名空间字符串
    String nsPath = namespacePath(namespace);
    try (Connection c = dataSource.getConnection();
            PreparedStatement ps = c.prepareStatement(selectSql)) {
        ps.setString(1, nsPath);
        ps.setString(2, key);
        try (ResultSet rs = ps.executeQuery()) {
            // 没有查到该命名空间+Key数据 → 返回null,代表KV不存在
            if (!rs.next()) {
                return null;
            }
            // 读取JSON字符串与版本号
            String json = rs.getString(1);
            long version = rs.getLong(2);
            // JSON反序列化为业务对象,封装成StoreItem返回
            return new StoreItem(key, deserialize(json), version);
        }
    } catch (SQLException e) {
        // SQL异常封装,转为运行时异常向上抛出
        throw new IllegalStateException("JdbcStore get failed", e);
    }
}

执行 SQL

sql 复制代码
SELECT value_json, version 
FROM agentscope_store 
WHERE namespace_path = 'agentsharness-demousers_defaultroot' 
  AND item_key = '/AGENTS.md'

namespace_path 中间的 不是普通短横线 / 下划线,是不可见分隔符字符(ASCII 单元分隔符,\u001F),用来拼接多级命名空间,agentsharness-demousersdemo-usersessions 分段说明:

分段序号 示例值 业务含义
第一段 agents 顶层业务域:区分 Agent 数据 / 全局配置数据 / 工具数据
第二段 harness-demo Harness 实例 / 项目标识,一套部署多个业务项目隔离
第三段 users 分类前缀:用户租户体系
第四段 _default 用户分组 / 租户组
第五段 root 具体用户 ID / 租户 ID

在数据库中的数据如下(需要手动创建):

3.3 创建 WorkspaceManager 工厂函数

提前捕获全局公共资源,定义一个可复用的 WorkspaceManager 工厂函数;后续传入 uid(用户ID)、sid(会话ID),就能快速生成租户 + 会话隔离的独立工作空间管理器,所有实例复用底层文件系统与索引,避免资源重复创建。

java 复制代码
            final AbstractFilesystem sharedFilesystemRef = filesystem;
            final Path capturedWorkspace = resolvedWorkspace;
            final WorkspaceIndex capturedIndex = workspaceIndex;
            BiFunction<String, String, WorkspaceManager> workspaceFactoryFn =
                    (uid, sid) -> {
                        RuntimeContext bakedRc =
                                HarnessAgentBuilderSupport.buildBakedRuntimeContext(uid, sid);
                        NamespaceFactory ctxNs =
                                rc -> (uid == null || uid.isBlank()) ? List.of() : List.of(uid);
                        AbstractFilesystem ctxFs =
                                new io.agentscope.harness.agent.filesystem.BakedContextFilesystem(
                                        sharedFilesystemRef, bakedRc);
                        return new WorkspaceManager(capturedWorkspace, ctxFs, capturedIndex, ctxNs);
                    };

3.4 WorkspaceContextMiddleware 创建与注册

3.4.1 初始化

java 复制代码
// HarnessAgent.java:1893-1900
if (!disableWorkspaceContext) {
    WorkspaceContextMiddleware markdownMw =
            new WorkspaceContextMiddleware(
                    wsManager,
                    name != null ? name : "ReActAgent",
                    environmentMemory,
                    maxContextTokens);
    markdownMw.setAdditionalContextFiles(additionalContextFiles);
    inner.middleware(markdownMw);
}

构造参数说明:

参数 来源 说明
wsManager 刚创建的 WorkspaceManager 统一管理文件读写
agentName builder.name() Agent 名称,默认 ReActAgent
environmentMemory builder.environmentMemory() 环境描述文本
maxContextTokens builder.maxContextTokens() Token 预算,默认 8000

3.4.2 Middleware 注册顺序

HarnessAgent.build() 中,Middleware 按以下顺序注册:

顺序 Middleware 是否注入 AGENTS.md 职责
用户 自定义 Middleware 可选 用户扩展点,最先执行
1 GracefulShutdownMiddleware --- 优雅关闭(ReActAgent 内置)
2 SandboxLifecycleMiddleware --- 沙箱生命周期
3 AgentTraceMiddleware --- 日志追踪
4 WorkspaceContextMiddleware` 注入 AGENTS.md / MEMORY.md / knowledge
5 AtPathExpansionMiddleware --- @path 文件引用展开
6 MemoryFlushMiddleware --- 记忆提取
7 MemoryMaintenanceMiddleware --- 记忆合并去重
8 CompactionMiddleware --- 对话压缩
9 ToolResultEvictionMiddleware --- 大工具结果卸载
10 SubagentsMiddleware / DynamicSubagentsMiddleware --- 子 Agent 编排
11 TaskReminderMiddleware todo_write 使用说明
12 PlanModeMiddleware Plan Mode 横幅
13 HarnessSkillMiddleware 技能列表 <available_skills>

注意 :用户自定义 Middleware 跑在最前面,可以看到最原始的 sysPrompt(尚未被 WorkspaceContextMiddleware 处理)。


3.5 HarnessAgent 构建完成

4. 调用阶段

每次 call() / stream() 时都会执行:

text 复制代码
用户调用
  │
  ├─ agent.call(msgs)                     ReActAgent.java:605
  ├─ agent.stream(msgs)                   ReActAgent.java:771
  └─ agent.streamEvents(msgs)             ReActAgent.java:771
         │
         ▼
  AgentBase.call(msgs)                    AgentBase.java:190
    → runLifecycle(msgs, this::doCall)    AgentBase.java:228
      → runLifecycleBody(msgs, rc, ...)   AgentBase.java:278
         │
         ├─ ① beforeAgentExecution(msgs, rc)
         │     → activateSlotForContext()  加载/创建 AgentState
         │     → 创建 CallExecution scope
         │     → 绑定 RuntimeContext, eventSink 等
         │
         ├─ ② notifyPreCall(msgs, scope)  AgentBase.java:713
         │     │
         │     ├─ seedSystemMsg(scope)     ReActAgent.java:517 ★ 核心入口
         │     │   → applySystemPromptMiddlewares(base, rc)  ReActAgent.java:536
         │     │     → WorkspaceContextMiddleware.onSystemPrompt() ★ AGENTS.md 注入
         │     │     → TaskReminderMiddleware.onSystemPrompt()
         │     │     → PlanModeMiddleware.onSystemPrompt()
         │     │     → HarnessSkillMiddleware.onSystemPrompt()
         │     │
         │     ├─ Hook.firePreCallEvent()  各 Hook 处理 system message
         │     │
         │     └─ consumeSystemMsgAfterPreCall(msg, scope)  存储到 scope.systemMsg
         │
         ├─ ③ doCall(msgs)                ReActAgent.java:834
         │     → doCallInner(msgs)         ReActAgent.java:1340
         │       → coreAgent()             ReActAgent.java:1722
         │         → executeIteration(0)   ReActAgent.java:1732
         │           → reasoning(0, false) ReActAgent.java:1735
         │             → hookDispatcher.firePreReasoning(state.context, systemMsg, modelName)
         │                 └─ 将 systemMsg 注入到模型输入消息列表首位
         │             → model.stream(messages)  发送给 LLM
         │
         └─ ④ saveStateToSession(scope)   持久化 AgentState

其中主要的流程,我们都在 Agent Scope Java 2.x 系列【20】Harness:系统提示词 中已经介绍过了,这里主要分析 workspaceManager 是如何获取到 AGENTS.md 内容的

4.1 入口

WorkspaceContextMiddleware

java 复制代码
// WorkspaceContextMiddleware.java:130-161
private String buildWorkspaceSection(RuntimeContext rc) {
    // ① 读取三大核心文件
    String agentsContent   = workspaceManager.readAgentsMd(rc).strip();       // AGENTS.md
    String memoryContent   = workspaceManager.readMemoryMd(rc).strip();       // MEMORY.md
    String knowledgeContent = workspaceManager.readKnowledgeMd(rc).strip();   // knowledge/KNOWLEDGE.md

    Path workspace = workspaceManager.getWorkspace();

    // ② 构建各部分
    String sessionContext = buildSessionContextSection(workspace, rc);
    String knowledgeBlock = buildKnowledgeBlock(rc, knowledgeContent, workspace);
    String additionalBlock = buildAdditionalContextBlock(rc);

    // ③ Token 预算计算
    int fixedTokens =
            estimateTokens(sessionContext)
                    + estimateTokens(agentsContent)      // AGENTS.md 计入固定开销
                    + estimateTokens(knowledgeBlock)
                    + estimateTokens(additionalBlock);
    int memoryTokens = estimateTokens(memoryContent);
    int available = maxContextTokens - fixedTokens;
    if (available > 0 && memoryTokens > available) {
        memoryContent = truncateToTokenBudget(memoryContent, available);  // 仅截断 MEMORY.md
    }

    // ④ 组装
    String workspaceParagraph = buildWorkspaceParagraph(workspace, ...);
    String loadedContext = buildLoadedContextSection(
            agentsContent, memoryContent, knowledgeBlock, additionalBlock, rc);
    return assembleSection(sessionContext, GUIDANCE_TEMPLATE, workspaceParagraph, loadedContext);
}

4.2 读取 AGENTS.md

workspaceManager.readAgentsMd 方法会调用到读取入口方法 readWithOverride

java 复制代码
private String readWithOverride(RuntimeContext rc, String relativePath) {
    // 1. 优先从带命名空间的文件系统读取
    String fsContent = readTextThroughFilesystem(rc, relativePath);
    if (!fsContent.isEmpty()) {
        return fsContent;
    }
    // 2. 文件系统无内容,降级读取本地磁盘原始文件
    return readFileQuietly(workspace.resolve(relativePath));
}

4.2.1 文件系统读取

readTextThroughFilesystem 会调用文件系统进行读取:

java 复制代码
    private String readTextThroughFilesystem(RuntimeContext rc, String filePath) {
        if (filesystem == null) {
            return "";
        }
        ReadResult r = filesystem.read(rc, filePath, 0, 0);
        if (!r.isSuccess() || r.fileData() == null) {
            return "";
        }
        String c = r.fileData().content();
        return c != null ? c : "";
    }

因为我们配置了 Mysql 文件系统,所以会进入到 CompositeFilesystem 中:

java 复制代码
    @Override
    public ReadResult read(RuntimeContext runtimeContext, String filePath, int offset, int limit) {
        RouteResult route = routeForPath(filePath);
        return route.backend().read(runtimeContext, route.backendPath(), offset, limit);
    }

首先也是获取路由结果:

然后也是两层读取逻辑:

java 复制代码
    @Override
    public ReadResult read(RuntimeContext runtimeContext, String filePath, int offset, int limit) {
        if (upper.exists(runtimeContext, filePath)) {
            return upper.read(runtimeContext, filePath, offset, limit);
        }
        return lower.read(runtimeContext, filePath, offset, limit);
    }

第一层 RemoteFilesystem 没有读取到内容时,会从 LocalFilesystem 中读取,路径为:

4.2.2 本地磁盘读取

readFileQuietly(workspace.resolve(relativePath)) 这里是重复操作,因为上一步中已经在本地读取过了,只是为了最终兜底:

最终 buildWorkspaceSection 读取到的内容:

4.3 Token 预算

Token 预算分配策略

text 复制代码
maxContextTokens (默认 8000)
├── sessionContext   (日期/OS/路径等,约 200 tokens)
├── AGENTS.md        (完整注入,不截断) ──┐
├── knowledgeBlock   (完整注入,不截断)   ├─ 固定开销 fixedTokens
├── additionalBlock  (完整注入,不截断) ──┘
│
└── MEMORY.md        (available = 8000 - fixedTokens)
                     (超出 available 则截断,追加截断提示)

核心要点AGENTS.md / KNOWLEDGE.md / additionalContextFile 始终完整注入 ,只有 MEMORY.mdToken 预算限制。

Token 估算方式:

java 复制代码
// WorkspaceContextMiddleware.java:394-395
private static int estimateTokens(String text) {
    return text == null || text.isEmpty() ? 0 : text.length() / 4;
}

简单启发式:4 个字符 ≈ 1 个 token

4.4 加载到系统提示词

WorkspaceContextMiddleware#buildLoadedContextSection 方法:

java 复制代码
/**
 * 组装完整的加载上下文 XML 片段,用于向大模型拼接当前 Agent 运行时上下文信息
 * 包含 Agent 会话上下文、记忆上下文、领域知识库、额外自定义补充内容四块数据
 * 整体包裹在 <loaded_context> 根标签内,作为系统上下文输入给模型
 *
 * @param agentsContent Agent 会话运行上下文文本内容
 * @param memoryContent 持久化记忆(memory 目录)相关上下文内容
 * @param knowledgeBlock 领域知识库/业务知识块文本内容
 * @param additionalBlock 额外补充自定义上下文片段,可为空
 * @param rc 当前运行时上下文(携带 uid、sid 租户会话信息,本方法暂未直接使用,预留扩展)
 * @return 拼接完成的 <loaded_context> 结构化 XML 字符串
 */
private String buildLoadedContextSection(
        String agentsContent,
        String memoryContent,
        String knowledgeBlock,
        String additionalBlock,
        RuntimeContext rc) {
    StringBuilder sb = new StringBuilder();
    sb.append("<loaded_context>\n");
    // 拼装 agents 会话上下文子节点
    sb.append(buildXmlContext("agents_context", agentsContent));
    // 拼装持久化记忆上下文子节点
    sb.append(buildXmlContext("memory_context", memoryContent));
    // 拼装领域知识库上下文子节点
    sb.append(buildXmlContext("domain_knowledge_context", knowledgeBlock));
    // 存在额外补充内容则直接追加(外部已保证是合法XML片段)
    if (!additionalBlock.isBlank()) {
        sb.append(additionalBlock);
    }
    sb.append("</loaded_context>\n");
    return sb.toString();
}

最终结构: