同样接入一个大模型 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 下的 WSL(Windows 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, ttl=ttl) 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_id="user_1001", ttl=10) 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(host="localhost", port=6379, db=0, decode_responses=True) def create_session_in_redis(user_id: str, ttl: int = 1800) -> str: import uuid session_id = uuid.uuid4().hex r.setex(f"session:{session_id}", ttl, user_id) return session_id def get_user_by_session(session_id: str): return r.get(f"session:{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(encoding="utf-8") meta, content = parse_yaml_front_matter(md_text) if not meta.get("name"): continue skills.append( Skill( name=meta["name"], description=meta.get("description", ""), version=meta.get("version", "0.0.1"), content=content, source_dir=skill_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_ascii=False) return json.dumps({"status": "订单不存在"}, ensure_ascii=False) @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_ascii=False) tool = TOOL_REGISTRY[name] try: result = tool(**arguments) if isinstance(result, (dict, list)): return json.dumps(result, ensure_ascii=False) return str(result) except TypeError as e: return json.dumps({"error": f"参数错误: {e}"}, ensure_ascii=False) except Exception as e: return json.dumps({"error": f"工具执行失败: {e}"}, ensure_ascii=False) 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(encoding="utf-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, key=score, reverse=True) 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_k=3) if docs: return "\n".join(doc["content"] for doc in