☰
从Demo到上线:用AgentScope打造生产级记忆型AI Agent
2026/11/3 22:19:46 网站建设 项目流程

最近总有朋友问我:AI Agent 到底怎么从 Demo 做到能上线?这个问题我很有发言权——我用 AgentScope 从零搭了一个带长期记忆的生产级 AI Agent,前后折腾了两个月,踩了不少坑,也沉淀了一套可以复用的方法。这篇文章就是整个项目从架构设计、代码实现到部署优化的全景复盘,核心关键词就两个:AI Agent、AgentScope。我会把记忆模块怎么做、RAG 怎么服务化、生产环境要处理哪些隐藏问题都讲清楚,适合那些已经跑通过大模型 API、想更进一步把智能体落地成真实业务系统的同学。

先说清楚这个项目解决了什么问题。普通 Demo 都是一问一答,聊完就忘,Agent 没有状态、没有工具、没有容错。而生产级记忆型 Agent 要面对的是:用户连续聊几十轮不丢上下文、跨会话还能记住用户偏好、需要的时候调用业务工具、模型偶尔抽风了系统还得稳定扛住。一句话,你要做的不是包一层 API,而是一个有记忆、有常识、有兜底的软件系统。

下面我按项目的实际推进顺序来写,从整体设计到核心实操,再到上线前的优化和排坑,你可以把它当成一份从 0 到 1 的参考手册直接对照着做。

1. 项目全景:先搞明白生产级 Agent 比 Demo 难在哪

1.1 你以为的 Agent 和实际要用的 Agent

很多人对 Agent 的第一印象是:给大模型写一段 system prompt,然后把用户的问题丢进去,让它自己发挥。Demo 阶段这么搞没问题,但你一旦面对真实业务,困难立刻冒出来。

首先是对话轮次问题。真实用户不会只问一句,他会连续追问,会把几小时前聊的内容翻出来问。这时候如果还是简单地把所有历史消息都塞给模型,上下文窗口很快就会被撑爆,Token 费用也会直线上升。我用一个例子说明:一个普通客服对话,如果聊天超过 30 轮,原始文本量大概在 4000 到 6000 字,塞进去不仅慢,而且模型容易“注意涣散”,开始忽略关键信息。

其次是状态问题。Demo 里模型是无状态的,但业务需要 Agent 记住用户叫什么、上次聊到哪、用户的偏好是什么,这些都是至少跨会话要保留的信息。没有记忆模块,每次对话都是“第一次见面”的陌生人,这种体验放在生产环境里是绝对不合格的。

然后是工具调用。生产级 Agent 光会聊天没用,它要能查订单、写工单、算价格。这就涉及大模型决定调用哪个工具、按什么格式传参、工具返回结果后怎么处理。每一步都有可能出错,模型可能把参数类型写错,可能编一个不存在的工具名,可能返回结果里有脏数据。这些在 Demo 里都能忍,在生产环境里都是事故。

最后是看不见的工程问题:日志、监控、限流、降级、安全。我见过太多团队把 Agent 流程写通了,结果模型一抖动,整个服务跟着挂。所谓生产级,就是把这些无聊但保命的工程问题全部处理掉。这个项目的目标就是把上述四个问题系统地解决。

1.2 技术选型:我为什么选了 AgentScope 而不是 LangChain

选型的时候我其实纠结了很久。LangChain 生态最大,文档和教程满天飞,但它的问题也恰恰是抽象层太多,光理解 Agent、Chain、Tool、Memory 这些概念之间的组合关系就要花不少时间。而且版本迭代激进,很多 API 换了版本就不能直接用,对生产项目来说维护成本偏高。

AutoGen 我也看了,它更偏多智能体对话研究,单人开发或者小团队做业务系统,会显得有点重。相比之下,AgentScope 有几个点特别对我胃口:

  • 概念更简单,核心可以理解为“消息 + 计算管线”,Agent 之间通过消息对象流转,调试起来很直观。
  • 模型接入方式统一,不锁死某一个厂商,OpenAI、Qwen、Ollama 这些都能接,我后面换模型成本很低。
  • 自带 Memory 和 Pipeline 机制,省去了自己拼轮子的时间。
  • 它在 2.0 版本里提出了 RAG as Service 的思路,把知识检索做成独立服务,而不是塞在 Agent 内部,这个架构我很认同。

这里我用一个表格总结一下我当时对比的几个框架的适用场景:

框架核心优势主要痛点适合场景
LangChain生态丰富,组件多抽象层多,版本变动大需要快速尝试大量外部集成
AutoGen多 Agent 对话编排强偏研究原型,运维成本高学术探索、复杂多角色模拟
AgentScope轻量直观,记忆与管线内建社区规模比前两者小一些业务型单 Agent / 多 Agent 落地

对我个人而言,生产项目最重要的是“出问题时能快速看懂自己在干什么”。AgentScope 的消息传递模型,让我可以直接打印出每一轮 Agent 内部处理了什么、调用了哪个工具、返回了什么内容,这比在抽象层里翻半天源码强太多了。如果你是小团队要快速上线一个智能客服或者业务助手,我建议认真考虑它。

1.3 记忆型 Agent 的核心需求拆解

在动手写代码之前,我把“记忆”这个词拆成了四个具体需求,这个拆解非常关键,因为后续所有设计都是围绕它展开的。

第一个是短期记忆,也就是当前会话内的对话序列。这个必须有,而且要有容量上限。我的设计里,最近 10 轮对话按原文保留,超过部分开始压缩,这部分后面会详细讲。

第二个是长期记忆,也就是跨会话的关键事实。比如用户的姓名、所在城市、订单偏好、历史诉求。这类信息必须持久化,而且需要支持按用户维度隔离和检索。

第三个是记忆检索。记忆不是存下来就完事了,模型要用的时候能快速找到相关的。这里就需要对长期记忆做向量化,然后在对话中按当前上下文去召回,说白了就是一个小型的召回系统。

第四个是记忆更新与淘汰。用户的偏好会变,过时的记忆必须能更新和清理,否则时间长了,Agent 会一直拿旧信息当依据,给你的答复就是“刻舟求剑”。

这四个需求如果不提前想清楚,写出来的记忆模块大概率是“对话历史无限累积”的玩具版。下面所有章节,都是围绕如何把这四条落地展开的。

2. 系统架构与设计思路

2.1 系统模块划分与数据流

我最终的系统架构可以概括为五个核心模块:入口网关、Agent 编排层、模型网关、记忆存储、工具与知识服务。入口网关负责接收用户请求和做基础的权限校验;Agent 编排层是核心逻辑所在,负责决定什么时候检索记忆、什么时候调用工具、什么时候把任务交给模型;模型网关统一封装各个模型厂商的 API,让上层 Agent 不用关心具体是哪个模型。

记忆存储承担了两类数据:一类是 Redis 里的会话级消息缓存,一类是向量数据库里的长期记忆和知识切片。工具服务是一些业务 API 的封装,比如查订单、查库存;而知识服务就是单独的 RAG Service,负责文档和知识库的检索。

整体的请求数据流是这样的:用户消息进来,Agent 编排层先把它写入当前 Session 的记忆缓冲区,然后带着当前消息和记忆缓冲区的内容去模型网关调用大模型。如果模型判断需要调用工具或检索知识,Agent 会把对应结果再作为新消息加入上下文,然后再调一次模型,直到模型输出最终答案。最后把完整对话和重要的信息回写到记忆存储。

你可能注意到了,这个流程没有做得很花哨,没有多 Agent 辩论,也没有复杂的规划器。我刻意保持简单,因为生产系统最重要的不是炫技,是每一步都可解释、可排查、可回滚。Agent 流程越复杂,排查一个问题要翻的链路就越长。

2.2 记忆分层设计:Session、Redis、向量库各管什么

我见过不少同学做记忆,直接把所有历史消息堆在一个 List 里,美其名曰“长上下文”。这样做的后果就是 Token 消耗不可控,而且真正重要的用户信息淹没在大量闲聊里,模型根本提取不出来。所以我把记忆拆成了三层,各管一摊。

第一层是 Session 上下文,放在内存或 Redis 里,只存最近 10 轮原始消息。这层负责让模型在单个会话里有连续的上下文,访问速度要快,容量严格限制。你可以理解成人的“短期工作记忆”,聊完这一阵子就开始淡忘。

第二层是会话摘要,放在 Redis 里,会为每个会话生成一个结构化的摘要。摘要内容包括用户是谁、这次聊了什么、最终结论是什么、有没有待办事项。会话结束或者达到一定轮数触发生成,Key 的设计是summary:{user_id}:{session_id}。这一层的存在是为了跨轮次压缩信息。

第三层是长期记忆,放在向量数据库里,按用户和会话来源做索引。每次会话结束后,我会从摘要里提取“值得长期记住的事实”,比如用户偏好和重要事件,把它们向量化后写入。在对话中,当新的用户输入进来,我会先从这层召回 Top 5 相关的历史记忆,再拼进上下文。

这样分层的逻辑其实很朴素:高频少量数据放最快的地方,低频大量数据放可检索的地方,中间用摘要做桥。时间长了也不用担心上下文爆掉,因为三层各有各的淘汰策略。

2.3 为什么采用 RAG as Service 而不是内嵌检索函数

AgentScope 2.0 提出 RAG as Service 后,我是第一批用起来的。我的项目里没有把检索逻辑写死在 Agent 内部,而是单独启动了一个 RAG 服务,供多个 Agent 实例共享。这样做的理由有三个,都很实际。

第一是逻辑解耦。Agent 的职责是推理和决策,知识检索是独立的“读资料”能力,两者混在一起会让 Agent 代码越来越臃肿。拆出来之后,检索策略的调整不必重新部署 Agent 服务。

第二是资源隔离。大规模文档检索很吃内存和 CPU,如果把它和 Agent 主流程放在同一个进程,一旦检索量上来,主服务的响应时间会直接被拖垮。独立部署之后,我可以单独给 RAG 服务扩容,两边互不干扰。

第三是多端复用。我同一个项目里后面还做了 API 助手、工单助手好几个 Agent,它们都需要查同一套知识库。如果把 RAG 做成服务,所有 Agent 共用一套接口,知识更新也只需要做一次,效率提升是实实在在的。

接口设计也很简单,核心就两个:一个是文档写入接口,负责把新文档分块、向量化、写入索引;另一个是检索接口,接收查询文本,返回相关片段列表。后面的实操章节我会展示怎么接。

3. 从零实操:在 AgentScope 里落地记忆型 Agent

3.1 环境准备与工程初始化

先说环境,我的推荐配置是 Python 3.10 以上,用 conda 单独建一个虚拟环境,避免和系统 Python 打架。包安装直接走 pip,但要注意 AgentScope 版本更新比较快,有些 API 细节会变,我的代码示例基于 2.0 系列版本,你实际用的时候记得对照一下官方 changelog。

conda create -n agentscope-demo python=3.10 conda activate agentscope-demo pip install agentscope pip install redis pip install qdrant-client

除了 Python 包,我建议把 Redis 装起来,长期记忆用的向量库我选的是 Qdrant,因为它是纯 Docker 部署,不用额外引入重型依赖,本地调试和上线都方便。启动命令也很简单:

docker run -p 6379:6379 redis:7 docker run -p 6333:6333 qdrant/qdrant

到这里基础环境就算准备好了。我在实际项目里还加了一个 .env 文件来管理模型 API Key,绝不让密钥出现在代码里。这个习惯希望你从一开始就建立起来。

3.2 模型接入与基础 Agent 搭建

AgentScope 的模型接入是在 init 阶段配置的,它把模型统一抽象成一个服务地址加一个模型名,好处是以后换模型不用动业务代码。下面这段代码展示了怎么配置一个大语言模型并创建最基础的 Agent:

import os import agentscope from agentscope.agent import ReActAgent # 配置模型,这里以兼容 OpenAI 协议的模型为例 model_config = { "config_name": "primary_model", "model_type": "openai", "model_name": "qwen-plus", "api_key": os.getenv("MODEL_API_KEY"), "base_url": os.getenv("MODEL_BASE_URL"), } agentscope.init(model_configs=[model_config]) # 创建 Agent,传入配置名 agent = ReActAgent( name="assistant", model_config_name="primary_model", sys_prompt="你是业务助手,回答问题时简洁、准确。" )

如果你用的是本地模型,比如 Ollama,base_url 指向本地地址就行,AgentScope 的模型层会把协议差异屏蔽掉。这里我额外强调一个理念:宁可先用默认配置跑通,再去做花哨功能。很多人一上来就想要自定义 prompt 模板和复杂的 Agent 状态机,结果连基础消息流转都没弄明白。

跑通基础 Agent 之后,你可以试一下直接调用并打印返回结果,观察消息对象的结构。AgentScope 里消息是一个 Msg 对象,包含 name 和 content 两个核心字段,理解了这个结构,后面所有记忆和工具相关的操作都顺理成章。

3.3 记忆模块的落地实现

基础 Agent 默认是不带记忆的,每次调用都是独立的。AgentScope 内置了 TemporaryMemory 可以快速给 Agent 加上会话内记忆,但对于生产级项目,我更推荐基于内置 Memory 类做自定义实现,把长期记忆的写入和召回也纳入到记忆模块里。

先看一个继承内置 Memory 类实现向量化长期记忆的最小示例:

from typing import Any from agentscope.memory import MemoryBase class LongTermMemory(MemoryBase): """基于 Qdrant 的长期记忆实现,按 user_id 隔离。""" def __init__(self, user_id: str, collection_name: str = "agent_memory"): self.user_id = user_id self.client = qdrant_client.QdrantClient(host="localhost", port=6333) self.collection_name = collection_name def add(self, messages, **kwargs): # 这里把 messages 中的关键内容向量化写入 Qdrant for msg in messages: # 省略:调用 embedding 模型生成向量 point = { "vector": embedding(msg.content), "payload": { "user_id": self.user_id, "content": msg.content, "time": msg.metadata.get("time", ""), } } self.client.upsert(collection_name=self.collection_name, points=[point]) def retrieve(self, query, k=5): # 召回当前用户相关的记忆 hits = self.client.search( collection_name=self.collection_name, query_vector=embedding(query), query_filter=models.FieldCondition( key="user_id", match=models.MatchValue(value=self.user_id), ), limit=k, ) return [hit.payload["content"] for hit in hits]

这段代码我给你几个关键注释:第一,每个记忆点都要带上 user_id,否则检索的时候会把所有用户的历史都拉出来,这在生产环境里属于严重事故。第二,写入的内容最好是经过提取的结构化文本,而不是把整个对话原文扔进去。一个小技巧是,在会话快结束时让模型输出一段“值得长期记住的事实”列表,再由代码写入记忆库,这样存储的噪音会小很多。

在 Agent 对话过程中,我会在调用模型之前先调retrieve(user_input),把召回的长期记忆拼进上下文,让模型感觉“它是记得你的”。这一步非常有效,实测用户体感提升非常明显。

关于短期记忆,我采用的策略是维护一个固定长度的消息队列,只保留最近 10 轮,超出部分触发摘要压缩,摘要存到 Redis,然后清掉旧消息。这个逻辑不复杂,但它是防止上下文无限膨胀的关键。

3.4 工具调用与知识库接入

生产级 Agent 必须能调用工具。AgentScope 的 ReActAgent 支持通过注册方式挂载工具函数,工具函数的参数要用类型注解描述清楚,模型会根据描述决定调用哪个工具以及传什么参数:

@agent.tool def query_order(order_id: str) -> dict: """查询订单状态 Args: order_id: 订单编号,形如 ORD20250101 Returns: 订单信息字典,包含状态、金额、物流单号 """ # 真实项目里这里调用内部订单 API return {"status": "shipping", "amount": 299.00, "tracking_no": "SF123456"}

注册好之后,ReActAgent 会在推理过程中自动判断是否需要调用它。这里有个特别实用的经验:工具描述和参数说明一定要写清楚,模型有自己选择工具的机制,说明写得模糊,它就容易传错参数。我最初在工具描述里没写订单编号格式,模型传了好几次无效的单号,通通失败。

再看看知识库接入。我这边 RAG 服务独立部署,提供两个 HTTP 接口,Agent 内部用函数封装来调用:

import requests def search_knowledge(query: str) -> list[str]: resp = requests.post( "http://rag-service:8000/retrieve", json={"query": query, "top_k": 3}, timeout=2.0, ) resp.raise_for_status() return [item["content"] for item in resp.json()["results"]]

然后把检索结果作为一个系统消息注入 Agent 的上下文,告诉模型“以下是知识库相关内容,请基于这些内容回答”。这个做法的要点是:不要把原始知识片段直接堆在用户问题后面,而是用明确的提示词标记出来,否则模型容易混淆“用户说的话”和“参考资料”。

我自己测试下来,带 RAG 的 Agent 在回答产品政策、操作步骤这类问题时,准确率比纯靠模型记忆高出一大截。所以如果你要做的 Agent 需要大量依赖业务文档,趁早把知识检索单独服务化,省得后面返工。

4. 生产级优化:上线前必须处理的问题

4.1 可观测性:别等线上事故才追日志

Agent 的每一次回答背后可能是多轮模型调用和多次工具调用,一旦出错,排查的复杂度比普通接口高了一个数量级。所以从第一天起,我就给每个请求设置了一个 trace_id,并用结构化日志记录每一轮的关键节点。

下面是建议的日志字段设计,你可以根据自己的业务再加:

{ "trace_id": "8f14e45fceea167", "session_id": "session_123", "user_id": "user_456", "stage": "tool_call", "tool_name": "query_order", "request": "ORD20250101", "response": {"status": "shipping"}, "duration_ms": 320, "model": "qwen-plus", "tokens": 1280 }

有了这个,出现问题时我可以一条命令把整个链路拉出来看,甚至能用日志来复现“当时模型为什么这么回答”的问题。AgentScope 本身也提供了内置的 monitor 模块,可以统计模型调用频率和 Token 消耗,这些数据对于估算成本非常有用。

我特别建议你在 Agent 编排层的入口和出口各打一条日志,一条记录原始用户输入,一条记录最终回复,中间的中间态用调试级日志。这样既不会让线上日志量爆炸,又能在需要的时候完整复现。

4.2 稳定性与降级:模型超时、工具异常、Token 超限

生产环境里模型 API 不可能永远稳定,超时、限流、返回格式异常都遇到过。我的做法是建立一套降级策略,从强到弱三层:

第一层是重试。对超时和 5xx 错误,间隔递增重试最多三次。第二层是精简上下文重试。如果重试仍然失败,很有可能是上下文太长导致首字延迟过大,那就压缩历史消息后再试一次。第三层是兜底回复。连续失败后,直接返回“当前服务繁忙,请稍后再试”,绝不把模型报错信息赤裸裸地抛给用户。

工具调用的容错也要单独处理。比如工具返回了非 JSON 格式,或者业务校验失败,我会把这些情况包装成一个错误消息重新喂给模型,让它换一种方式处理。这在 ReAct 框架里很常见,相当于给模型一次“自我修正”的机会。

并发控制同样不可忽视。我在网关层引入了一个简单的信号量机制,限制同时进行中的模型调用数,超出部分排队等待。否则高峰期一群用户同时请求,模型网关和下游服务器都会被瞬间打满。

4.3 安全性:工具白名单与提示注入

Agent 的安全问题我一开始没当回事,直到有人试出可以绕过提示词,让 Agent 调用一个本不该调用的工具,我才意识到这个问题的严重性。

解决分三个方面。第一,工具白名单机制。Agent 内部维护一个工具列表,不是注册了就能随便调,而是只能调用白名单内的函数。第二,操作类工具二次确认。涉及发送消息、提交订单这类会改变状态的工具,代码里强制增加一个用户确认步骤,模型不能擅自执行。第三,提示注入防护。把用户输入和系统指令严格分开,用户内容是数据不是指令,工具返回结果也不能直接覆盖系统 prompt,真要引入外部的信息时,统一走参考资料通道并标明来源。

这三个东西在 Demo 阶段可以不加,但在生产环境是必须的。我不希望 Agent 真正上线之后,成为业务系统里的一个安全后门。

5. 常见问题与排查技巧实录

5.1 记忆串台:不同用户之间互相看到对方历史

这个问题的表现是用户 A 的对话里出现了用户 B 的订单信息,非常严重,排查起来又很隐蔽。根本原因基本都一样:记忆模块是全局单例,没有按 user_id 做隔离。尤其在 AgentScope 里,如果你直接在一个全局 Memory 实例上 add 消息,它会被所有 session 共享。

解决方式我在 3.3 节已经埋了伏笔:每个用户或者每个会话都要创建独立的记忆实例。如果是临时对话,直接 new 一个;如果是长期记忆,在向量检索时要带 user_id 过滤条件。排查时先看日志,确认写入和检索时用的 user_id 是否一致。我遇到过一种情况是会话开始拿错 user_id,导致后面所有写入都进了别人的桶,这种问题看日志很容易发现。

5.2 Agent 回复变慢:上下文越来越长

症状是对话头几轮响应很快,到二十轮之后首字延迟明显上升。原因不用猜,Memory 无限增长,每次请求带的上下文从几千 Token 涨到一万多,模型处理时间自然水涨船高。

解决思路就是前面说的分层记忆:保留最近 10 轮原始消息,超出部分触发生成摘要,摘要和长期记忆向量化之后从上下文里移除原文。另外要设置模型输入的上限,例如最大 8000 Token,超过就强制触发压缩流程,不能等模型自己崩溃。

这类“响应变慢”问题最好在生产前做压测时发现,不要等用户抱怨了再去查日志。我的经验是:模拟一个 30 轮对话跑一遍,观察 Token 数和响应时间变化,立刻就能看出来记忆策略是否合格。

5.3 工具返回 JSON 解析失败

这是 ReAct Agent 最常见的坑之一。模型说要调用query_order,返回的参数是合法的函数调用,但工具返回的内容是 JSON 字符串,Agent 内部解析时因为格式不规范崩了。常见原因包括模型返回了带 markdown 标记的 JSON(例如包裹在代码块里)、字符串里有未转义的特殊字符、或者 JSON 嵌套太深。

我针对这个问题做了两件事。一是在工具描述里明确写出返回格式规范,并且给了一个示例,让模型按标准 JSON 返回。二是在解析失败时,做一个简单的修复层,去掉多余的换行和代码块标记,如果仍然失败就返回错误信息让模型重新生成。说到底,工具解析不能假设一定成功,你要把它当作一个随时可能出错的第三方依赖来对待。

5.4 RAG 检索不到相关内容

检索为空的问题,大多数时候不是代码 bug,而是数据问题。我刚接知识库时,把一篇操作手册分成 4 个固定大小的大块,结果检索“退款问题”老是召回不到“售后流程”段落。后来把 chunk 改小到 400 字左右,并且把标题和上下文保留在 chunk 里,召回率立刻上来了。

另外,embedding 模型的选择也很关键。通用模型对业务术语理解有限,如果你的知识库专业名词很多,建议试试领域微调过的 embedding 模型。还有一个常用手段是调低相似度阈值,我默认用 0.5,如果检索结果稀疏,就降到 0.3。

我把这个排查过程整理成一个速查表,方便你以后直接对号入座:

现象可能原因处理方式
检索结果为空chunk 太大或太小调整为 400 字左右,保留上下文
召回结果不相关embedding 模型不适合领域尝试更贴合业务场景的 embedding 模型
召回了但 Agent 不用没有把检索结果拼进系统消息检查上下文注入逻辑和 prompt 表述
检索延迟高RAG 服务和主服务没分离独立部署,并按需扩容

5.5 最后说几句实在话

项目做到这个程度,我最大的体会是:开发 Agent 最难的从来不是“让模型说一句聪明的话”,而是让它稳定地记住该记住的、稳定地调用该调用的、稳定地在自己搞不定时给出体面的失败。Demo 阶段的代码可以天马行空,但生产级系统需要的是纪律和边界。

如果你看完这篇文章准备自己动手,我建议不要一上来就追求大而全,先从单一场景切进去,比如先做一个带短期记忆和两个工具的内部助手,跑通之后再逐步加入长期记忆和 RAG 服务。AgentScope 的文档和社区已经比我刚开始用的时候完善很多,遇到 API 变动也不要慌,核心的架构思路比具体接口更重要。

最后再分享一个小技巧:在 Agent 开发里,把“用户的问题、模型的思考过程、工具的返回结果、最终的答案”这四类信息全部结构化地记下来,你就有了一份宝贵的调试素材库。每次出问题,第一件事不是改代码,而是去翻这个素材库,找出模型决策错在哪一步。坚持这个习惯,你的 Agent 会越调越聪明。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询