
学习 Hermes Agent 这类 AI 大模型侧的 Agent 工程时最容易踩的坑不是模型不会回答而是把 Session 会话、Skill 技能、工具调用和上下文加载当成四件独立的事。实际跑一个能用的 Agent这四部分必须围绕同一条请求链路协作用户输入进入后系统先加载会话和历史上下文再把可用技能与工具描述放进提示词模型决定调用哪个工具工具执行结果回填给模型最后产出回复并持久化会话。本文会从这四个核心概念开始用一个最小可运行的 Python 工程把它们串起来并给出环境配置、目录设计、代码实现、运行验证、常见问题和生产部署建议。这套内容适合三类读者第一次接触 Agent 开发想理解 Session、Skill、Tool 和上下文加载之间关系的开发者已经能调用大模型 API但觉得多轮对话和工具调用难以组织的人准备在本地或测试环境部署 Hermes Agent想先建立工程化思维的技术人员。1. 先搞懂 Agent 的四个关键概念再动手写代码1.1 Hermes Agent 不是模型而是一套 Agent 运行工程很多资料把 Agent 简单理解成“接一个大模型 API 就能自动完成任务的程序”这个理解会直接导致后期代码乱。模型只负责推理和文字生成真正让 Agent 具备状态管理、技能扩展、工具调用能力的是外面这一层运行框架。Hermes Agent 的学习重点是 Session 会话管理、Skill 技能注册、Tool 工具调用、Context 上下文加载这四层。它们各自解决一个具体问题Session 记录“之前发生过什么”。Skill 告诉模型“当前具备哪些专项能力”。Tool 把模型的决定变成真实可执行的函数。Context 决定“这一次请求到底能把哪些信息放进输入”。如果一上来就写 model.chat.completions.create代码很短但后续加一个技能、接一个工具、做一次会话恢复都会变得非常难受。先理解这四层后面的部署和排错才有依据。1.2 Session 会话不是聊天记录而是完整运行状态很多开发者在 Web 项目里用过 Session容易下意识把 Agent Session 等同于浏览器会话。两者概念接近但 Agent 里的 Session 需要保存的内容更复杂至少包括会话 ID。模型名称和参数快照。多轮 user、assistant 消息。中间出现的工具调用和结果。技能执行状态和上下文引用。更新时间、来源渠道等元数据。在纯 API 调用中如果你把每条消息每次请求都重新传给模型确实也能实现多轮对话。但这里的问题是状态不可控用户清空历史时如何同步工具执行产生中间结果时如何回填切换模型后旧历史还能不能用Session 的职责就是把这些问题统一收纳到结构化存储中。1.3 Skill 技能负责把“会做什么”注入给模型Skill 的粒度比 Tool 大。一个 Skill 往往是一套能力目录里面写明它适合处理什么任务、需要调用哪些 Tool、应该按什么顺序执行。例如笔记助手是一个 Skill它内部可以包含三个 Toollist_notes、read_note、save_note。模型只有在用户请求涉及笔记时才激活这个 Skill激活后 Prompt 中会出现这个技能的说明。不要把所有技能一次性全塞进系统提示词模型在超长上下文中反而会丢失关键指令。Skill 注册表的价值就是按需装配。1.4 Tool 工具负责把模型决定变成可执行动作模型本身不能直接操作文件系统、数据库或外部 API。Tool 就是模型与真实世界的桥梁。模型不会自己执行代码它只会输出一次“结构化决定”例如{ name: save_note, arguments: {\title\: \Session学习笔记\, \content\: \Session状态要先持久化再继续下一轮对话\} }Agent 侧拿到这个结果后去本地函数注册表里找到 save_note执行它再把执行结果转成消息返回给模型。缺少这一步模型能力再强也只能在文字世界内打转。1.5 Context 上下文加载决定模型这次能看到什么模型是无状态推理引擎每次请求是否能看到历史、工具结果、外部知识完全取决于 Agent 如何构造 messages。合理做法是分层组装System 层固定系统角色和全局规则。Skill 层当前任务激活的技能说明。History 层最近且未超过预算的历史消息。ToolResult 层本次链路刚产生的工具执行结果。User 层用户本次输入。Retrieval 层如果需要外挂知识库则把命中片段插入到历史消息前后。Context 拼接顺序对模型理解影响很大。工具结果必须紧跟调用它的助理消息否则模型无法把结果对应到具体动作历史消息过长时优先丢弃中间步骤保留最近意图和最终结论。2. 环境准备与目录设计先搭一个可复现的最小工程2.1 版本和环境要求不建议一开始就在不确定的前提下使用最新版桌面安装包。任何 Agent 工程都应该先记录一套可复现的环境组合出现问题时才能定位是代码问题还是依赖问题。项目建议要求说明操作系统Windows 10/11、Ubuntu 20.04、macOS后续命令以 Linux/macOS 为主Windows 注意路径写法Python3.10 或更高需要 dataclass、类型注解等现代语法包管理pip、venv避免直接装到系统 Python模型接口OpenAI 兼容接口支持配置 base_url便于切换本地模型服务磁盘空间预留 500MB 以上仅代码学习本地模型另算网络能访问模型 API 即可生产环境还需考虑访问控制和审计如果实际下载的 Hermes Agent 安装包自带运行环境优先按官方 README 的版本要求执行。下面给出的目录和代码用于说明这套工程逻辑落到真实项目时需要根据安装包的版本和包路径调整。2.2 创建虚拟环境并安装依赖先建立项目目录再创建虚拟环境。mkdir hermes-agent-lab cd hermes-agent-lab python -m venv .venv source .venv/bin/activateWindows 环境把激活命令换成.venv\Scripts\activate安装基础依赖python -m pip install --upgrade pip pip install openai python-dotenv pyyamlopenai 是官方 SDKpython-dotenv 用来读取 .env 配置pyyaml 用来读取 Skill 技能描述文件。如果你的环境不需要 YAML 技能目录可以暂时不装 pyyaml但建议保留后面做技能注册时会更方便。2.3 项目目录结构与 .env 配置设计合理的目录结构能让 Session、Skill、Tool、Context 各归其位。本项目建议如下hermes-agent-lab/ ├── .env ├── .env.example ├── requirements.txt ├── app/ │ ├── __init__.py │ ├── main.py │ ├── session.py │ ├── context.py │ ├── skills.py │ └── tools.py ├── data/ │ ├── notes/ │ └── sessions/ └── skills/ ├── meeting_review/ │ ├── skill.yaml │ └── instructions.md └── chat_helper/ └── skill.yaml.env 示例HERMES_API_KEYsk-your-key HERMES_BASE_URLhttps://api.openai.com/v1 HERMES_MODELgpt-4o-mini HERMES_SESSION_DIRdata/sessions HERMES_NOTE_DIRdata/notes不要提交 .env 到 Git提交时只保留 .env.example。API Key 必须通过环境变量注入不要把密钥写死在代码里。如果使用本地大模型服务把 HERMES_BASE_URL 改成对应的 OpenAI 兼容地址即可例如HERMES_BASE_URLhttp://127.0.0.1:8000/v1不同模型对函数调用、JSON 输出和控制符的处理能力不同切换模型前要先确认模型是否支持原生 tools 参数。2.4 先验证模型连接再进入功能开发在写复杂代码前先写一段最小连接验证import os from dotenv import load_dotenv from openai import OpenAI load_dotenv() client OpenAI( api_keyos.getenv(HERMES_API_KEY), base_urlos.getenv(HERMES_BASE_URL), ) resp client.chat.completions.create( modelos.getenv(HERMES_MODEL, gpt-4o-mini), messages[ {role: system, content: 你是 Hermes Agent 的连通性测试助手。}, {role: user, content: 请回复连接正常}, ], ) print(resp.choices[0].message.content)这一步能确认 Key、Base URL、模型名三者是否匹配。很多安装和接入问题都出在这个环节而不是后面的 Agent 代码。3. Session 会话的代码落地用 JSON 文件做可恢复的状态存储3.1 定义 Session 数据结构为了不过度设计先把 Session 建模成 dataclass。这里的关键是messages 必须是结构化列表每个元素都是 API 可直接消费的字典而不是把整段对话存成一个大字符串。在 app/session.py 中写入from dataclasses import dataclass, field, asdict import json import os import time dataclass class Session: session_id: str model: str messages: list[dict] field(default_factorylist) metadata: dict field(default_factorydict) created_at: float field(default_factorytime.time) updated_at: float field(default_factorytime.time) def add_user_message(self, content: str): self.messages.append({role: user, content: content}) self.updated_at time.time() def add_assistant_message(self, content: str): self.messages.append({role: assistant, content: content}) self.updated_at time.time() def to_dict(self): self.updated_at time.time() return asdict(self) classmethod def from_dict(cls, data): return cls(**data)注意不要把 role 之外的自定义字段混进 messages。像“这条消息属于哪个技能”“这条消息的 token 数”这类信息应放入 metadata而不是消息体里。API 对 messages 有严格结构要求混合业务字段可能让兼容本地模型的接口直接报错。3.2 SessionStore保存、恢复、防止路径穿越Session 的保存位置由 HERMES_SESSION_DIR 控制。每个会话一个 JSON 文件文件名使用 session_id。class SessionStore: def __init__(self, session_dir: str data/sessions): self.session_dir session_dir os.makedirs(session_dir, exist_okTrue) def _path(self, session_id: str) - str: if session_id ! os.path.basename(session_id): raise ValueError(session_id 不能包含路径分隔符) return os.path.join(self.session_dir, f{session_id}.json) def save(self, session: Session): path self._path(session.session_id) tmp path .tmp with open(tmp, w, encodingutf-8) as f: json.dump(session.to_dict(), f, ensure_asciiFalse, indent2) os.replace(tmp, path) def load(self, session_id: str) - Session: path self._path(session_id) if not os.path.exists(path): return Session(session_idsession_id) with open(path, r, encodingutf-8) as f: data json.load(f) return Session.from_dict(data)这里有一个容易被忽略的安全细节session_id 必须经过 os.path.basename 校验否则恶意调用可能通过 ../../ 之类路径访问其他文件。Session 文件本质是本地数据但生产环境里如果 session_id 来自外部这就是路径穿越漏洞入口。3.3 为什么保存要使用 tmp 文件加 renameSessionStore.save 先写 .tmp 文件再调用 os.replace 替换正式文件。直接打开正式文件写入如果进程中途崩溃或磁盘写入失败原文件可能变成半截 JSON。os.replace 在同一个文件系统内是原子操作要么新内容完全生效要么旧内容保持不变。这个技巧在保存 Session、技能缓存和知识库索引时都建议保留。恢复会话时可以这样验证store SessionStore(data/sessions) s store.load(demo-001) print(len(s.messages)) s.add_user_message(继续上一次话题) store.save(s)多次运行程序后再次打开还能从 data/sessions/demo-001.json 恢复历史这就是 Session 持久化的意义。4. Skill 技能注册和 Context 上下文加载按需提示比堆指令更可靠4.1 SkillRegistry用目录和描述文件注册技能很多开发者喜欢把所有指令写在 system prompt 里结果技能一多提示词首屏被挤爆模型反而丢失关键信息。这里用目录方式注册 Skill。每个技能目录里放一个 skill.yaml描述技能名称、使用条件和工具清单。例如 skills/meeting_review/skill.yamlname: meeting_review description: 当用户要求整理会议纪要、记录项目复盘或保存讨论结论时使用 prompt: | 你是会议与项目复盘助手。用户提出保存或总结需求时先判断是否涉及已保存笔记。 如果需要查看已有内容调用 list_notes 和 read_note。 需要落盘时调用 save_note 写入 Markdown 文件并给用户返回文件路径。 tools: - list_notes - read_note - save_noteSkillRegistry 的作用是扫描目录、读取 YAML、把可用技能暴露给 ContextBuilder。代码放在 app/skills.pyimport os from dataclasses import dataclass import yaml dataclass class Skill: name: str description: str prompt: str tools: list[str] | None None class SkillRegistry: def __init__(self, skill_dir: str skills): self.skill_dir skill_dir self._skills: dict[str, Skill] {} def load(self): for root, _, files in os.walk(self.skill_dir): if skill.yaml not in files: continue yaml_path os.path.join(root, skill.yaml) with open(yaml_path, r, encodingutf-8) as f: data yaml.safe_load(f) skill Skill( namedata[name], descriptiondata.get(description, ), promptdata.get(prompt, ), toolsdata.get(tools, []), ) self._skills[skill.name] skill return self def get_skill_text(self, skill_names: list[str] | None None) - str: lines [] for name, skill in self._skills.items(): if skill_names and name not in skill_names: continue lines.append(f技能名{skill.name}) lines.append(f适用场景{skill.description}) lines.append(f执行说明\n{skill.prompt}) return \n\n.join(lines)在这个实现里技能说明是否注入到 System prompt由 skill_names 参数决定。最简单的激活策略是把所有技能说明都注入更精细的做法是在用户输入前先做一次轻量分类只激活相关技能。4.2 ContextBuilder把会话历史、技能说明、用户输入组装成一次请求ContextBuilder 需要解决两个问题历史顺序不能乱上下文长度不能超。代码放在 app/context.pyclass ContextBuilder: def __init__(self, base_system_prompt: str, max_history_chars: int 8000): self.base_system_prompt base_system_prompt self.max_history_chars max_history_chars staticmethod def _estimate_cost(message: dict) - int: text message.get(content) if isinstance(text, str): return len(text) 64 return 128 def build(self, session, user_input: str, skill_text: str , extra_context: str ) - list[dict]: system_prompt self.base_system_prompt if skill_text: system_prompt \n\n可用技能说明\n skill_text if extra_context: system_prompt \n\n检索参考内容\n extra_context messages [{role: system, content: system_prompt}] recent [] used 0 for m in reversed(session.messages): cost self._estimate_cost(m) if used cost self.max_history_chars: break recent.append(m) used cost recent.reverse() messages.extend(recent) messages.append({role: user, content: user_input}) return messages这段代码里最关键的是 max_history_chars 预算。历史消息从后往前选择优先保留最近的对话从前往后截断会丢掉用户刚刚表达的意图导致模型出现“失忆感”。4.3 上下文加载里不要忽略技能说明的排序System prompt 建议顺序是角色定位、任务规则、技能说明、工具使用约束、输出格式、安全边界。技能说明不能放在用户消息之后。如果模型已经读完一条具体用户请求才知道“你拥有笔记工具”它对工具的感知会明显降低。Agent 开发和 Prompt 调试的核心原则是先给模型完整操作手册再让模型看到具体任务。5. Tool 工具调用闭环从模型输出结构化参数到真实执行5.1 先注册安全、可验证的本机工具工具选择对演示效果很关键。这里选择三个笔记类工具避免引入网络请求和系统命令降低教学风险。代码放在 app/tools.pyimport json import os import re from pathlib import Path class ToolRegistry: def __init__(self): self._handlers {} self.schemas [] def register(self, name, description, parameters, handler): self._handlers[name] handler self.schemas.append({ type: function, function: { name: name, description: description, parameters: parameters, }, }) def execute(self, name: str, arguments: dict): if name not in self._handlers: raise KeyError(f未知工具{name}) return self._handlers[name](**arguments) def has_tools(self) - bool: return len(self.schemas) 0再定义三个实际函数def _safe_note_path(note_dir: str, title: str) - Path: filename re.sub(r[^a-zA-Z0-9_-], _, title) .md path Path(note_dir) / filename return path def list_notes(note_dir: str) - dict: path Path(note_dir) path.mkdir(parentsTrue, exist_okTrue) files [p.name for p in path.glob(*.md)] return {ok: True, count: len(files), files: files} def save_note(note_dir: str, title: str, content: str) - dict: path _safe_note_path(note_dir, title) path.parent.mkdir(parentsTrue, exist_okTrue) path.write_text(content, encodingutf-8) return {ok: True, path: str(path)} def read_note(note_dir: str, title: str) - dict: path _safe_note_path(note_dir, title) if not path.exists(): return {ok: False, error: 笔记不存在} return {ok: True, content: path.read_text(encodingutf-8)}save_note 使用正则把标题里的特殊字符替换成下划线防止路径穿越。工具函数返回 dict 的好处是结构清晰能直接写入 Tool 消息。在主程序中完成注册registry ToolRegistry() registry.register( namelist_notes, description列出笔记目录下所有 Markdown 笔记文件名, parameters{ type: object, properties: {}, }, handlerlambda: list_notes(NOTE_DIR), ) registry.register( namesave_note, description把内容保存为一条 Markdown 笔记标题只用字母数字下划线连字符, parameters{ type: object, properties: { title: { type: string, description: 笔记标题不带扩展名, }, content: { type: string, description: Markdown 正文, }, }, required: [title, content], }, handlerlambda title, content: save_note(NOTE_DIR, title, content), )工具描述写得好不好直接决定模型调用准确率。描述要写清触发条件、参数格式、边界限制不能只写“保存笔记”。没有说明文件路径规则时模型可能生成带斜杠的标题。5.2 AgentRuntime发起请求、接收工具调用、回填结果、循环收敛工具调用的本质是循环不是单次请求。第一轮模型可能返回 tool_callsAgent 执行后把结果追加进去再发起第二轮请求。代码可以直接写在 app/main.pyimport json from openai import OpenAI class AgentRuntime: def __init__(self, client: OpenAI, session, store, context_builder, tool_registry, model: str): self.client client self.session session self.store store self.context_builder context_builder self.tools tool_registry self.model model def run(self, user_input: str) - str: messages self.context_builder.build( self.session, user_input, skill_text, ) while True: kwargs {model: self.model, messages: messages} if self.tools.has_tools(): kwargs[tools] self.tools.schemas resp self.client.chat.completions.create(**kwargs) assistant_msg resp.choices[0].message tool_calls getattr(assistant_msg, tool_calls, None) assistant { role: assistant, content: assistant_msg.content or , } if tool_calls: assistant[tool_calls] [] for tc in tool_calls: assistant[tool_calls].append({ id: tc.id, type: function, function: { name: tc.function.name, arguments: tc.function.arguments, }, }) messages.append(assistant) if not tool_calls: self.session.add_user_message(user_input) self.session.add_assistant_message(assistant_msg.content or ) self.store.save(self.session) return assistant_msg.content or for tc in tool_calls: try: args json.loads(tc.function.arguments or {}) result self.tools.execute(tc.function.name, args) result_payload {ok: True, data: result} except Exception as exc: result_payload {ok: False, error: str(exc)} messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result_payload, ensure_asciiFalse), })这段循环有几个重要工程点工具执行失败不要直接抛异常应该把错误信息作为 tool 消息返回给模型让模型决定是修正参数重试还是向用户说明失败。每次循环都要把 assistant 消息先放进 messages再放 tool 消息顺序反了模型会把工具结果挂到错误对话上。只有最终得到普通回复时才写入 Session工具调用中间过程不长期保存否则 Session 文件会迅速膨胀。5.3 完整的 CLI 入口在 app/main.py 最后补上入口def main(): from app.session import SessionStore from app.context import ContextBuilder from app.tools import ToolRegistry session_store SessionStore(data/sessions) session session_store.load(demo-001) session.model MODEL registry ToolRegistry() # 注册三个笔记工具 builder ContextBuilder( base_system_prompt你是运行在本地的 Hermes Agent 助手使用中文回答工具返回值作为辅助事实。, max_history_chars6000, ) runtime AgentRuntime( clientclient, sessionsession, storesession_store, context_builderbuilder, tool_registryregistry, modelMODEL, ) print(Hermes Agent 已启动输入 exit 退出) while True: user_input input(你 ).strip() if user_input.lower() in (exit, quit): break answer runtime.run(user_input) print(Agent , answer) if __name__ __main__: main()这个入口不是为了“能跑就行”而是让每个组件都有明确生命周期。Session 在程序启动时加载Context 按轮构建Tool 使用全局注册表模型调用集中在 AgentRuntime。真实项目在这些类之间补充日志、监控和配置中心时会更顺畅。6. 运行验证通过一次“保存笔记再读取”确认完整链路6.1 预期运行过程启动程序前先用命令行确认环境变量已加载python -c from dotenv import load_dotenv; load_dotenv(); import os; print(bool(os.getenv(HERMES_API_KEY)))输出 True 表示 .env 配置读取成功。然后执行python -m app.main第一次输入你 请保存一条 Session 使用笔记标题是 session_os_replace内容是保存会话时要先写 tmp 文件再用 os.replace 替换。模型会先返回一次工具调用。可以打印日志确认类似信息[ToolCall] save_note title: session_os_replace content: 保存会话时要先写 tmp 文件再用 os.replace 替换。 [ToolResult] {ok: True, path: data/notes/session_os_replace.md} Agent 已保存到 data/notes/session_os_replace.md继续输入你 读取刚才那条笔记并说明里面讲了什么这次模型会调用 read_note再基于返回内容总结。6.2 Session 文件验证程序退出后查看会话目录ls data/sessions cat data/sessions/demo-001.json文件里应包含 user 消息和 assistant 消息工具调用过程不在 messages 中持久化。这是有意设计避免 Session 文件被工具输出撑爆。6.3 验证检查清单检查项预期结果失败时排查方向API 连通能返回一次普通回复Key、Base URL、模型名Tool schema 生效模型不要求说明也会自动调用 save_note工具描述不清晰、模型不支持原生 toolsTool 参数正确生成的标题不含斜杠和空格参数 description 要明确规则Tool 结果回填模型能引用保存成功后的文件路径检查 tool_call_id 是否与 assistant 消息一致Session 保存再次启动后能读取历史消息检查 data/sessions 权限和 JSON 文件是否完整Context 截断长历史不会导致 context length exceeded调低 max_history_chars或引入摘要压缩完整的验证不是只确认“程序启动了”而是确认模型确实感知到工具返回结果并且能把结果用于下一轮回答。如果模型执行完 save_note 后仍然回答“我不知道你有没有保存成功”说明 Tool 消息回填链路有问题优先检查消息顺序和 tool_call_id。7. 常见问题与排查路径7.1 安装或首次启动时要求登录、网页授权不少 Agent 产品在安装后第一次启动时会要求打开网页完成账号绑定或 API Key 授权。这不是安装失败而是产品把用户身份和模型服务绑定在一起。处理顺序完整查看启动页提示的链接不要在终端里反复回车跳过。打开网页完成登录后回到应用点击“已完成授权”或重启程序。如果显示授权成功但状态未变检查安装目录、系统代理、防火墙设置。如果只想学习 Agent 工程原理可以直接用本文这类纯 Python 工程不依赖桌面安装包。7.2 报错 context length exceeded这个错误出现时模型输入超过了窗口长度。先看报错发生在哪一步如果发生在知识库内容加载后说明检索片段太长如果发生在多轮对话后期说明 Session 历史没做截断。解决方法是在 ContextBuilder 中压缩历史同时限制检索片段返回长度。需要注意中文 token 估算不能用简单字符数但本地先用字符数做粗粒度预算再用实际 token 数调整是性价比最高的改法。7.3 Tool 调用结果看起来没问题但模型还在乱编出现这种现象通常不是因为模型笨而是模型没有看到完整结果。原因集中在三处Tool 返回内容过长被截断模型只看到了前半段。Tool 消息和 assistant tool_call 没有正确对应。工具结果只写在日志里没有作为消息发回模型。检查时把消息列表完整打印出来按角色和顺序检查system user assistant(tool_calls) tool(tool_call_idxxx) assistant(final)只要 assistant tool_call 后缺少 tool 消息模型在下一轮就只能猜测工具执行结果。7.4 Session 文件保存失败或 JSON 损坏先检查 data/sessions 目录是否存在、进程是否有写权限。再查看代码是否直接覆盖正式文件强烈建议保留 tmp 加 os.replace 的写法。如果并发场景需要多个线程或进程同时写同一 Session还需要引入文件锁或改为数据库存储不能只在单进程脚本里做一次性覆盖。问题现象常见原因处理建议安装环境后 import openai 失败虚拟环境未激活或依赖没装好执行 pip list 检查重新激活 venvAPI Key 读取为空.env 不在当前目录或未调用 load_dotenv打印 os.getenv 结果确认工作目录模型返回内容中文乱码系统默认编码问题文件统一 UTF-8终端切换 UTF-8工具参数在中文标题下报错文件系统非法字符用白名单规则替换符号禁止直接拼路径多次运行后 Session 暴涨只做追加不做压缩设置历史条数和字符数上限8. 部署和生产化建议从能 demo 到能上线中间还差四件事8.1 学习环境与生产环境的差异能力学习环境生产环境配置.env 固定读本地配置中心或环境变量注入Session 存储JSON 文件Redis、PostgreSQL支持多实例日志print结构化日志带请求和会话 ID安全本地信任鉴权、限流、审计、密钥加密上下文粗粒度截断Token 预算 历史摘要 向量检索发布源码直接运行容器化、镜像版本、回滚方案minimal demo 能跑通不代表可以直接暴露到公网。生产环境里 Session 文件按用户名隔离、Tool 白名单、文件路径校验、模型输出敏感词过滤、操作审计每一条都不能少。8.2 Skill 和 Tool 的扩展原则Skill 越少越精确。每新增一个技能都要评估它是否增加了信息量还是只是重复了其他