大模型应用开发:Agent架构中的会话、技能、工具与上下文管理

发布时间:2026/9/5 15:58:42
大模型应用开发:Agent架构中的会话、技能、工具与上下文管理 同样接入一个大模型 API有的团队几周就能上线一个能处理真实业务的智能助理有的团队折腾了一个月还停在一问一答的 Demo 阶段。真正拉开差距的往往不是模型本身而是模型外围的那套工程体系。如果你正在做 AI 大模型应用开发大概率已经遇到过这些需求对话要能记住上一轮内容能查询企业内部订单能读取外部文档后回答问题还要在用户身份发生变化时正确处理会话状态。这些能力单靠“写 Prompt”是解决不了的它们分别对应 Agent 架构里的四件事Session 会话管理、Skill 技能机制、Tool 工具调用、Context 上下文加载。本文以 Hermes Agent 作为切入场景把这四层从概念、配置到代码完整串一遍。即使你之后不使用 Hermes Agent而是换到 LangChain、扣子Coze、阿里云百炼或自己从零搭建 Agent 服务这套分析框架和工程经验依然适用。需要提前说明的是Hermes Agent 相关工具的迭代速度很快不同源头发布的 CLI 版、桌面版、源码版之间可能存在差异。为了避免误导本文不会死记某个具体版本号和某个“唯一标准 API”凡是与具体发行版强相关的操作我会明确标注“以你安装版本为准”。1. 为什么需要 Hermes Agent 这类 Agent 运行时1.1 大模型本身的三个边界先看一个很常见的现象。直接调用大模型接口你问它“帮我查一下订单 20260888001 走到哪了”模型大概率会告诉你“作为 AI 我无法实时查询订单”。这不是模型不够聪明而是模型本身存在三个边界。第一无状态。大模型接口默认不会记得你上一轮说了什么每次请求都是独立的。如果不做 Session 管理多轮对话根本没法成立。第二无实时数据。模型训练完成后知识就停留在某个时间点。它不知道当前天气、不知道你数据库里的订单状态也不掌握企业内部文档。第三无操作能力。模型只能输出文本不能帮你调 API、改数据库、发邮件。即便它知道业务流程也无法真正执行。解决这三个边界不能靠模型能力要靠工程框架。1.2 Hermes Agent 解决什么问题把大模型理解成一个“大脑”Agent 要做的是给这个大脑装上眼睛、耳朵和手脚。Hermes Agent 这类运行时本质上就是一套把模型能力封装成智能体的工程框架它要解决的核心问题包括会话生命周期如何创建会话、保存上下文、处理过期清理。技能扩展如何让 Agent 拥有特定领域问题的处理能力。工具接入如何让模型安全地调用外部 API、数据库、文件系统。上下文组织如何把系统提示词、历史消息、检索知识拼装成一次高质量的模型请求。在开源社区里围绕 Claude Code、Codex 等工具经常会提到 Skill 概念。Hermes Agent 在这方面走了类似路线用声明式的技能文件、标准化的工具注册表把“模型能做什么”变成可维护、可插拔的工程资产。1.3 适合哪些场景从落地场景看以下几类项目很适合优先考虑 Agent 架构。第一类企业内部知识库问答。文档分散在多个系统需要先检索再回答还要能标注信息来源。第二类业务系统智能助手。比如订单查询、售后处理、库存咨询模型负责理解用户意图具体数据操作通过工具完成。第三类办公流程自动化。让 Agent 根据指令读取表格、生成周报、整理会议纪要。本文后面会以一个“支持会话记忆和订单查询的知识问答服务”为例带大家从头到尾落地。2. Hermes Agent 环境准备与部署要点2.1 运行环境与版本说明由于 Hermes Agent 存在多种发行形态本文不强行规定某个操作系统。如果你在本地开发推荐以下环境项目建议操作系统Windows 10/11、Ubuntu 20.04/22.04、macOS开发语言Python 3.10 或 3.11交互方式命令行、桌面版、HTTP API 三种形态大模型服务本地部署的开源模型或云端大模型 API如果你的项目计划在服务器上长期运行建议优先使用 Linux 环境并通过 systemd、Docker 或进程守护工具来维护生命周期。Windows 下的 WSLWindows Subsystem for Linux也可以作为开发调试环境但要注意 systemd 问题的坑这一点会在第 8 章展开。2.2 安装方式参考不同版本的安装方式差异较大但通常逃不出下面几种。第一种是源码安装。下载源码后在项目根目录执行# 请以你下载到的源码目录中的 README 为准 python -m venv .venv source .venv/bin/activate # Windows 下使用 .venv\Scripts\activate pip install -r requirements.txt第二种是桌面版安装包。这种方式一般在图形界面内完成双击安装即可。安装报错的重点排查方向是安装包完整性、系统权限、缺少 VC 运行库或 .NET 依赖等问题。第三种是直接通过包管理器安装。如果官方发布了 pip 包通常是pip install hermes-agent这里提醒一下不要在网上随便复制一条安装命令就执行。务必先确认包名与你的发行渠道一致再检查校验值。涉及生产环境时最好在隔离的虚拟环境或容器内安装。安装完成后可以通过查看帮助信息验证环境是否可用hermes-agent --help如果命令不存在说明可执行文件没有加入 PATH需要根据安装目录手动配置环境变量。2.3 首次启动与登录流程Hermes Agent 第一次启动时往往会要求初始化配置文件部分版本会引导登录一个网站完成设备授权。很多同学第一次遇到这个界面会担心是不是安装包有问题其实不是。这通常是标准的设备授权流程Device Authorization Flow。本机会生成一个一次性授权码并提示你在浏览器打开某个网址、输入授权码完成身份绑定。这样做的好处是本地程序不需要直接保存你的账号密码网页端确认后把授权凭证返回给本地客户端即可。如果你所在的内网环境不能访问外部登录页面可以查看配置中是否提供离线模式或自定义模型服务商入口。有的发行版登录只是为了拉取插件市场或模型配置离线使用时可以跳过但前提是你的模型服务已经能正常访问。2.4 回到主页面和常用操作不少用户在进入某个子功能后不知道怎么返回主页面。对于 CLI 版本一般可以通过输入home、menu或/home返回桌面版则通常有左侧菜单或顶部面包屑。但不同版本差异明显最靠谱的办法是输入?或help查看当前版本支持的命令列表。死记硬背某个命令反而容易换一个版本就失效。3. Session 会话管理精讲3.1 Session 在 Agent 架构里的作用Session 是 Agent 应用中最基础、也最容易做错的一层。从功能上看Session 至少承担三个职责。第一个职责是保存多轮对话上下文。用户说“刚才那个问题再解释一下”服务端需要知道“那个问题”指的是什么。第二个职责是保存用户身份与登录态。请求从哪里来、属于哪个用户、能访问哪些数据都需要会话作为载体。第三个职责是串联工具调用链条。在一次 Agent 任务里模型可能连续调用多次工具每一次调用的中间结果、状态流转都应该挂载到同一个 Session 上。如果没有 Session你看到的 Agent 就是一个“金鱼记忆”的聊天窗口无法接入真实业务。3.2 Cookie、Session、Token 的区别很多初学者会把 Cookie、Session、Token 混为一谈。这里用一个表格讲清楚。机制存储位置核心特点常见使用场景Cookie客户端浏览器/本地文件每次请求自动携带有过期时间记录登录标识、偏好设置Session服务端服务端保存状态客户端只保存 Session ID登录后的状态管理、购物车Token客户端服务端验签无状态服务器不保存携带权限声明移动端、分布式系统鉴权在 AI Agent 场景里Session 的重要性更加突出。因为 API 请求本身是无状态的但业务上下文必须是有状态的。你可以把 Session ID 下发给前端存储然后前端每次请求都携带这个 ID。客户端存的是 ID真正的对话历史、用户信息存服务端。更安全的做法是在浏览器场景下把 Session ID 放入 HttpOnly Cookie前端 JavaScript 无法读取降低被脚本窃取的风险。3.3 会话存储方案设计Session 存储方案一般有三级演进路径。第一级单机开发环境直接放内存。优点是简单、快缺点是一重启就丢失而且无法横向扩展。第二级多实例部署后使用 Redis。把 Session 集中存储任何一台服务实例都能读取适合中小规模生产。第三级超大规模或强一致性场景可以引入数据库表或使用 Redis 加数据库双写。但从成本考虑大多数 Agent 应用不需要走到这一级。3.4 代码实战实现一个 SessionManager下面给出一个可直接运行的轻量 SessionManager支持创建、查询、删除和过期清理。# 文件路径agent_demo/session_manager.py import time import uuid import threading class Session: def __init__(self, session_id: str, user_id: str, ttl: int 1800): self.session_id session_id self.user_id user_id self.data {} self.created_at time.time() self.expire_at self.created_at ttl def is_expired(self) - bool: return time.time() self.expire_at def touch(self, ttl: int 1800): self.expire_at time.time() ttl class SessionManager: def __init__(self): self._sessions {} self._lock threading.Lock() def create_session(self, user_id: str, ttl: int 1800) - Session: session_id uuid.uuid4().hex session Session(session_id, user_id, ttlttl) with self._lock: self._sessions[session_id] session return session def get_session(self, session_id: str): session self._sessions.get(session_id) if session is None: return None if session.is_expired(): with self._lock: self._sessions.pop(session_id, None) return None session.touch() return session def delete_session(self, session_id: str): with self._lock: self._sessions.pop(session_id, None) def cleanup(self): expired_ids [ sid for sid, s in self._sessions.items() if s.is_expired() ] with self._lock: for sid in expired_ids: self._sessions.pop(sid, None) return len(expired_ids) if __name__ __main__: manager SessionManager() s manager.create_session(user_iduser_1001, ttl10) print(create session:, s.session_id) got manager.get_session(s.session_id) print(load session:, got.user_id)这段代码的核心是给每个会话设置了有效期默认 30 分钟无操作自动过期。每次调用get_session时自动续期相当于滑动过期策略。如果部署到生产环境只需要把_sessions换成一个 Redis 客户端。下面是用 Redis 保存用户 ID 的等效思路import redis r redis.Redis(hostlocalhost, port6379, db0, decode_responsesTrue) def create_session_in_redis(user_id: str, ttl: int 1800) - str: import uuid session_id uuid.uuid4().hex r.setex(fsession:{session_id}, ttl, user_id) return session_id def get_user_by_session(session_id: str): return r.get(fsession:{session_id})这里要注意redis.Redis的构造函数参数在不同版本中基本稳定但如果你使用的是 Redis Cluster 或带密码的实例需要把连接参数替换成实际环境的值。4. Skill 技能机制与自定义技能开发4.1 Skill 到底是什么Skill 是 Agent 体系里一个非常重要的抽象但很多人对它理解不够。简单说Skill 就是给 Agent 预装的一个“标准化动作包”。它不是一段普通的系统提示词而是一个带触发条件、执行步骤、示例说明甚至附带脚本的完整模块。举个例子。如果你希望 Agent 能处理“退款申请”你当然可以在系统提示词里写“当用户申请退款时请按下面步骤操作”。但随着技能越来越多系统提示词会越来越长维护成本快速上升。Skill 方案的做法是把退款流程单独写成一个技能目录包括退款条件、审核路径、政策链接并在技能描述里写清楚“什么时候应该使用这个技能”。当用户消息进来后Agent 先做意图识别发现匹配再动态加载技能内容把它追加到当前上下文。4.2 Skill 的标准结构参考社区常见的 Agent 技能设计一个标准技能通常包含元信息、描述、正文和可选示例。推荐目录结构skills/ └── order_query/ ├── SKILL.md └── examples/ └── demo.jsonSKILL.md是技能的核心文件Markdown 格式头部用 YAML 声明元信息正文写执行步骤和注意事项。--- name: order_query description: 查询订单状态、物流信息。当用户询问“我的订单到哪里了”“订单发货没有”“物流进度”时使用。 version: 1.0.0 --- # 订单查询技能 1. 从用户消息中抽取订单号。 2. 调用订单查询工具 get_order_status 获取订单状态。 3. 若订单存在将状态、时间、物流信息整理成用户友好文案。 4. 若订单不存在向用户确认订单号是否正确。 ## 注意事项 - 订单号可能是 12 位数字也可能带字母前缀。 - 查询失败时不要臆造结果。这里的description非常关键。它是模型判断“该不该使用这个技能”的依据写得越具体命中率越高。4.3 代码实战技能加载器下面实现一个简单的技能加载器自动读取skills目录下的所有技能文件。# 文件路径agent_demo/skill_loader.py from dataclasses import dataclass from pathlib import Path import re dataclass class Skill: name: str description: str version: str content: str source_dir: Path def parse_yaml_front_matter(md_text: str): match re.match(r^---\s*\n(.*?)\n---\s*\n(.*)$, md_text, re.S) if not match: return {}, md_text meta_text, content match.groups() meta {} for line in meta_text.strip().splitlines(): if : in line: key, value line.split(:, 1) meta[key.strip()] value.strip() return meta, content.strip() def load_skills(skill_root: str ./skills) - list: skills [] root Path(skill_root) if not root.exists(): return skills for skill_dir in root.iterdir(): skill_file skill_dir / SKILL.md if not skill_file.exists(): continue md_text skill_file.read_text(encodingutf-8) meta, content parse_yaml_front_matter(md_text) if not meta.get(name): continue skills.append( Skill( namemeta[name], descriptionmeta.get(description, ), versionmeta.get(version, 0.0.1), contentcontent, source_dirskill_dir, ) ) return skills if __name__ __main__: for skill in load_skills(): print(skill.name, |, skill.description)这个加载器把技能从文件系统里读出来并在之后的主流程中进入候选列表。4.4 为什么技能不生效技能写了但 Agent 总是不调用是新手最高频的问题。常见原因有三个。一是描述太泛。比如描述写“处理订单问题”但用户消息是“退货退款政策是什么”两者无法建立强关联。二是同质技能互相干扰。两个技能都覆盖订单场景模型判断成本变高容易选错。三是技能正文缺少步骤。技能正文没有给出可执行的判断逻辑模型只能泛泛理解。排查时先看技能名称是否准确再看描述是否覆盖真实问法最后看技能内容是否有可执行步骤。技能的本质是可复用的行为模板不是文档收藏夹。5. 工具调用Tool Calling与外部系统集成5.1 工具调用的原理工具调用是 Agent 能够“做事”的关键环节。在大型语言模型的应用接口中它通常被称作 Function Calling 或 Tool Calling。它的工作流程并不神秘可以拆成五步将用户消息发送给模型同时附带“可用工具清单”。模型判断当前请求是否需要调用工具如果需要就输出一个结构化调用请求包含工具名和参数。Agent 收到模型输出后不去直接拼接文本而是去真实执行对应的函数。函数返回结果交给 Agent。Agent 将工具结果再次发给模型生成最终的用户回复。关键在于模型只负责“决定调用哪个函数、填入什么参数”真实的函数代码仍然运行在你自己控制的进程里。5.2 工具注册与函数实现先看一个最简单的工具实现这里用装饰器把函数注册进全局表。# 文件路径agent_demo/tools.py import json TOOL_REGISTRY {} def register_tool(func): TOOL_REGISTRY[func.__name__] func return func register_tool def get_order_status(order_id: str) - str: 模拟查询订单状态。 生产环境中这里应该改成真实的数据库或订单系统 API。 mock_data { 20260888001: {status: 已发货, logistics: 顺丰 SF1234567890}, 20260888002: {status: 待支付}, } if order_id in mock_data: return json.dumps(mock_data[order_id], ensure_asciiFalse) return json.dumps({status: 订单不存在}, ensure_asciiFalse) register_tool def get_refund_policy(product_type: str 默认) - str: policy { 默认: 七天无理由退货需商品完好, 食品: 食品类非质量问题不支持七天无理由退货, } return policy.get(product_type, policy[默认])同时我们需要给模型提供一个工具说明清单描述每个函数的作用和参数结构。这个结构采用当前主流大模型接口通用的 JSON Schema 风格[ { type: function, function: { name: get_order_status, description: 查询订单状态与物流信息, parameters: { type: object, properties: { order_id: { type: string, description: 订单号 } }, required: [order_id] } } }, { type: function, function: { name: get_refund_policy, description: 查询商品售后与退款政策, parameters: { type: object, properties: { product_type: { type: string, description: 商品类型 } } } } } ]5.3 工具调用分发器接下来实现一个分发器它不关心工具的具体逻辑只负责根据函数名和参数完成调用并统一做异常处理。# 文件路径agent_demo/tool_dispatcher.py import json from agent_demo.tools import TOOL_REGISTRY def dispatch_tool(name: str, arguments: dict) - str: if name not in TOOL_REGISTRY: return json.dumps({error: f未找到工具: {name}}, ensure_asciiFalse) tool TOOL_REGISTRY[name] try: result tool(**arguments) if isinstance(result, (dict, list)): return json.dumps(result, ensure_asciiFalse) return str(result) except TypeError as e: return json.dumps({error: f参数错误: {e}}, ensure_asciiFalse) except Exception as e: return json.dumps({error: f工具执行失败: {e}}, ensure_asciiFalse) if __name__ __main__: print(dispatch_tool(get_order_status, {order_id: 20260888001})) print(dispatch_tool(not_exist_tool, {}))实际调用时如果模型返回了结构化工具调用你可以用类似下面的方式去执行# 伪代码示意具体字段名以模型服务商的接口文档为准 # tool_call completion.choices[0].message.tool_calls[0] # tool_name tool_call.function.name # tool_args json.loads(tool_call.function.arguments) # result dispatch_tool(tool_name, tool_args)5.4 工具调用的安全边界工具调用是 Agent 能力最强的部分也是最危险的部分。一旦 Agent 可以调用真实工具Prompt 注入和误操作的风险就会立刻上升。以下几点必须记牢第一工具必须白名单化。只能调用显式注册的函数不能支持“把任意代码作为工具执行”。第二危险操作必须二次确认。比如删除数据、发送对外消息、转账支付这类操作不能由模型单方面决定必须加入人工审批环节。第三数据库操作要做权限控制。Agent 服务连接数据库时应使用最小权限账号并限制只能操作指定表。生产环境禁止使用 root 或管理员账号供 Agent 调用。第四所有调用要留审计日志。谁在什么时间触发了一次工具调用、参数是什么、结果如何都要可回溯。6. 上下文加载与外挂知识库6.1 上下文窗口不是越大越好大模型的上下文窗口越来越长但“长”不等于“好”。把大量无关内容塞进上下文既增加成本也会稀释模型对关键信息的注意力。更好的做法是分阶段组织上下文系统提示词区放 Agent 身份、行为约束、技能加载结果。会话历史区放最近的几轮对话必要时把更早的消息压缩成摘要。检索知识区放从知识库或文档里检索出来的相关内容。工具返回区放最近一次工具调用的结构化结果。6.2 外挂知识库的价值很多项目需要让 Agent 基于企业内部文档回答问题又不可能把整个文档库都塞进提示词。这就催生了外挂知识库它的标准流程包括文档清洗去掉页眉页脚、水印等噪音。文本分块控制每个块的大小。向量化并写入向量数据库。用户提问时对问题做向量检索或关键词检索。将 Top K 个相关块拼接到 Prompt 中让模型结合文档回答。在实际项目中你可以使用开源向量库、云厂商向量检索服务也可以使用传统数据库的全文索引。核心思路是一致的先检索再回答。6.3 代码实战轻量知识检索为了方便快速跑通这里提供一个不使用外部向量库的极简检索实现。它通过关键词重叠度打分适合没有复杂语义要求的起步版本。# 文件路径agent_demo/knowledge.py from pathlib import Path def split_text(text: str, chunk_size: int 200, overlap: int 20) - list[str]: if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) start end - overlap return chunks def build_knowledge_from_file(file_path: str) - list[dict]: text Path(file_path).read_text(encodingutf-8) chunks split_text(text) return [{id: i, content: chunk} for i, chunk in enumerate(chunks)] def retrieve(query: str, chunks: list[dict], top_k: int 3) - list[dict]: query_words set(query.strip().split()) def score(chunk: dict) - int: return sum(1 for word in query_words if word in chunk[content]) scored_chunks sorted(chunks, keyscore, reverseTrue) return scored_chunks[:top_k] if __name__ __main__: # 使用前请准备一个纯文本文件 chunks build_knowledge_from_file(./knowledge/policy.txt) results retrieve(退货退款, chunks) for r in results: print(命中片段:, r[content][:80])这个版本的检索准确率远不如向量检索但它能帮你把“检索、拼接、回答”的链路先跑通。后面可以在不改变主流程的情况下把retrieve函数内部替换成向量召回逻辑。6.4 上下文被截断的应对策略上下文超长是 Agent 工程的经典问题。第一种应对策略是滑动窗口。只保留最近的 N 轮对话更早的对话丢弃。第二种策略是历史摘要。每过几轮让模型把之前的对话压缩成一段摘要之后把摘要作为上下文的一部分。第三种策略是保护核心提示词。在拼接上下文时系统提示词永远排在最前面检索知识紧接其后历史消息放在最后。如果长度超限优先截断历史消息而不是丢弃系统指令。7. 项目实战带会话、技能、工具的知识问答服务7.1 需求说明综合前面的知识点我们来实现一个小型 Agent 服务。它需要满足三个功能支持多用户会话同一用户连续提问时能记住上下文。当用户询问订单或物流时调用订单查询工具。当用户询问售后政策时从知识文件检索相关内容并回答。为了便于演示大模型调用部分使用一个可替换的call_llm函数。你可以将它替换为任意大模型服务商接口或本地部署模型接口。7.2 项目结构agent_demo/ ├── main.py ├── requirements.txt ├── session_manager.py ├── skill_loader.py ├── tools.py ├── tool_dispatcher.py ├── knowledge.py ├── skills/ │ └── order_query/ │ └── SKILL.md └── knowledge/ └── policy.txt7.3 服务端核心代码main.py是整个服务的入口我把它拆成可读性较好的几个步骤。# 文件路径agent_demo/main.py from fastapi import FastAPI from pydantic import BaseModel from agent_demo.session_manager import SessionManager from agent_demo.skill_loader import load_skills from agent_demo.knowledge import build_knowledge_from_file, retrieve from agent_demo.tool_dispatcher import dispatch_tool app FastAPI() session_manager SessionManager() # 启动时加载技能与知识 SKILLS load_skills(./skills) KNOWLEDGE_CHUNKS build_knowledge_from_file(./knowledge/policy.txt) SESSION_HISTORY_LIMIT 6 class ChatRequest(BaseModel): user_id: str message: str def call_llm(messages: list) - str: 在实际项目中将这里替换成你使用的大模型接口。 不需要在本文件暴露厂商 SDK 细节便于后续切换服务商。 # 示例代码默认返回一个固定提示防止没有配置模型时报错 # 真正接入大模型后这段逻辑会被替换 prompt_text \n.join([m.get(content, ) for m in messages]) if 订单 in prompt_text: return 已为你查询订单信息请稍候。 return 模型尚未接入请打开 call_llm 函数。不过为了让案例更接近真实 Agent我们再往前一步。在进入call_llm之前我们先做一次意图判断决定是走工具、走知识检索还是直接回复。# 文件路径agent_demo/main.py续 def build_system_prompt() - str: skill_lines [] for skill in SKILLS: skill_lines.append(f[技能 {skill.name}] {skill.description}) return f你是一个智能客服助手。你可以使用以下技能 {chr(10).join(skill_lines)} 当用户询问订单信息时调用工具查询当用户询问政策时参考知识库内容。 def handle_tool_query(message: str): if 订单 in message or 物流 in message or 发货 in message: import re order_id_match re.search(r\d{11}, message) if order_id_match: order_id order_id_match.group() return dispatch_tool(get_order_status, {order_id: order_id}) return None def handle_knowledge_query(message: str): if 退货 in message or 售后 in message or 政策 in message: docs retrieve(message, KNOWLEDGE_CHUNKS, top_k3) if docs: return \n.join(doc[content] for doc in