Java 服务已经有用户系统、数据库和业务工具,接入 Harness 时最关心的通常不是如何多写一个循环,而是会话与工作区如何按用户组织、已有业务能力怎样进入 Agent。
本文固定源码提交 c5db8f72dbea5c2c70de96885e4167ce5b1b24b0。根构建版本标记为 2.0.3-SNAPSHOT,而博客已有 Maven 示例固定的是 2.0.1。两者不是同一版本:源码解读以下述提交为准,历史示例保留其依赖,不把新接口无条件抄入旧工程。本轮没有编译或调用模型。
HarnessAgent 外面包了什么
HarnessAgent持有一个 ReActAgent delegate,在其外围增加工作区、记忆、Skill、文件系统和其他中间件。这样读者可以把核心模型工具循环与环境装配区分开。
从 Builder.build 开始看,比从一个 getter 开始更有效。这个提交先复制 Toolkit,避免不同构建实例的工具注册相互污染,再检查本地、远端与沙箱文件系统配置是否冲突,然后确定 Workspace 与存储,并装配中间件和工具。
当配置 DistributedStore 时,代码可以接入相应存储组件,但文件系统模式仍由使用者选择。共享状态存储不等于自动选择了沙箱,单个“分布式”选项也不包办所有运行职责。
一次 call 如何带上会话身份
call(Msg, RuntimeContext) 转向列表形式,补齐有效会话上下文,再通过包装调用进入 delegate。RuntimeContext 中的 userId、sessionId 帮助定位状态和工作区视图。
但它们只是调用参数,业务服务必须确保来源可信。若客户端可以任意提交别人的 userId,框架按这个 ID 正确寻址也会形成数据泄露。认证与授权应发生在应用入口,不能交给模型填写。
同一会话的并发还需要明确控制。拥有两个不同的请求对象,不代表底层状态没有共享;多个服务副本使用本地 JSON 存储,也不会自然看到同一份最新状态。
Workspace 文件怎样进入模型输入
WorkspaceContextMiddleware实现 onSystemPrompt,调用 WorkspaceManager 读取规则、记忆和知识入口,再组装上下文片段。
知识目录不必整棵内联。此提交会组织 KNOWLEDGE.md 内容与其他文件路径,供模型进一步定向读取。例如入口说明“售后条款在 policy.md”,模型知道去哪里找,但尚未读到文件的全部细则。
代码把可能阻塞的文件读取放在 boundedElastic 调度器上。它解决阻塞工作放在哪里执行,不意味着目录自动隔离,也不意味着异步调用永远不会超时。读 Reactor 代码时,应把执行调度与业务顺序分开理解。
规则文件被装配后仍然是模型输入。若需要禁止修改记忆或导出敏感文件,执行入口和文件系统策略要继续参与。模型看见一句“不要修改”,不构成操作系统拒绝写入的证据。
工作区内容不是自动出现的
下面摘录 WorkspaceContextMiddleware.buildWorkspaceSection 的两个读取点,增加中文说明。它们依赖原方法中的 rc,不是可单独运行的示例。
// 根据当前 RuntimeContext 读取适用规则,并清理外围空白。
String agentsContent = workspaceManager.readAgentsMd(rc).strip();
// 读取知识入口;其他知识文件还会被组织成路径目录供按需读取。
String knowledgeContent = workspaceManager.readKnowledgeMd(rc).strip();
AGENTS.md 之所以生效,是因为这些读取结果随后被装配到系统提示。若自定义配置禁用了相关中间件,文件仍在磁盘上,却不会沿这条路径进入请求。这种因果关系比“框架支持某文件”更有排查价值。
Skill 的发现、合并与加载
HarnessSkillMiddleware在本提交中拥有 SkillRuntime。它按来源顺序合并同名 Skill,应用可见性筛选,处理资源位置,构建目录,再将目录绑定到当前 RuntimeContext。
随后向模型提供可用 Skill 说明,并注册 load_skill_through_path 等加载能力。这样主体与资源可以按需获取,而不是启动时加载全部内容。读者可以继续进入SkillRuntime观察目录和加载工具之间的连接。
同名覆盖不是神秘的模型判断,而是装配阶段的合并规则。生产需要记录最后选择的来源和版本,否则用户目录的同名 Skill 可能让同一任务呈现不同工作方式。
缓存资源也要跟随身份作用域。不能以为技能只有公开说明就忽略其中可能携带的私有参考资料。源码中的作用域设计需要与应用租户模型一起审查。
状态、Transcript 与文件为何分开
AgentStateStore 保存运行状态;Transcript 记录会话经过;Workspace 保存长期文件与资料。Builder 中可以看到 TranscriptMiddleware、CompactionMiddleware 和 ToolResultEvictionMiddleware 等不同组件,它们各自处理记录、压缩与大结果。
例如压缩后,模型输入变短,但原始会话日志不必被删除;大工具结果可以外置,但引用指向的文件要仍然可访问。恢复不仅取回一段聊天,还需要核对文件和外部动作。
当前源码提供 workspaceFor(userId, sessionId) 返回绑定的视图,避免为了某一次调用随意修改共享 WorkspaceManager。理解这种写法,可以帮助 Java 服务避免把“当前用户”放进全局可变字段造成串数据。
本地文件系统与 Sandbox 不可混称
Builder 区分不同文件系统配置,并在相应条件下增加 SandboxLifecycleMiddleware。选择本地环境时,执行动作可能在宿主权限下发生;选择沙箱还需要具体 Provider、资源约束和回收机制。
写 .workspace(path) 只说明工作区位置。配置 userId 与 sessionId 只说明命名空间。只有实际执行设施与授权检查落实后,才能讨论租户隔离保证。
同样,设置压缩触发条数只是历史管理策略。二十条消息可能很短,也可能包含巨大工具输出,条数阈值不能替代 Token 和结果体积预算。已有示例中的数值会注明为演示参数。
用现有 Java 工程观察一次请求
HarnessWorkspaceDemo.java下载保留 2.0.1 的基础接入方式,补充中文注释和与本系列一致的订单资料。它演示工作区读取与模型调用,不能声称已经包含强制独立 Verifier、线上授权或完整持久恢复。
若要研究本篇快照的新接口,应检出指定提交,使用其自带构建与Workspace 示例。不要把 2.0.3-SNAPSHOT 当成必然可从公共仓库解析的发布版本。
将业务完成门槛接入时,应独立实现报告解析、输入对照与交付记录。配置中的 sysPrompt 只能解释工作要求,不能自动生成这些检查代码。
生命周期与资源释放在哪里收尾
当前 HarnessAgent 实现 AutoCloseable,close 处理其拥有的运行资源,应用应在服务生命周期内安排关闭。单次命令示例与长期 Web 服务的对象寿命不同,不能在每个请求后关闭仍被共享的实例。
源码中部分接口返回 Mono;调用是否订阅、在哪里等待、取消如何传递,都影响动作是否真正发生。命令行可以在入口等待结果,响应式服务器则应保持异步链并使用适当的超时与资源管理。不要在事件循环线程中随意阻塞。
四个项目放在一起怎样理解
Pi 有助于看清执行循环和扩展入口;Deep Agents 展示中间件与 Backend 的装配;DeepSeek Harness 展示插件依赖与生命周期;AgentScope Java 展示工作区和会话能力如何进入应用框架。
它们不能仅按功能列表互换。实际选择需要检查语言、环境、状态、权限和扩展点,并用自己的任务验证。至此,读者已经可以从一条用户请求追到模型、工具、文件和交付依据,而不是只认识 Harness 这个词。