基于LLM的Agent制作实战:从设计原则到生产环境部署

1次阅读
没有评论

共计 2867 个字符,预计需要花费 8 分钟才能阅读完成。

image.webp

背景痛点分析

最近在做一个客服 Agent 项目时,踩了不少坑。总结下来当前 LLM Agent 开发主要存在三大问题:

基于 LLM 的 Agent 制作实战:从设计原则到生产环境部署

  1. 状态管理混乱:很多实现把对话历史直接拼接成字符串传给 LLM,当对话轮次超过 10 轮后,不仅 token 开销爆炸,关键上下文还容易丢失。上周我们就遇到用户问了 5 个问题后,Agent 完全忘记最初需求的尴尬情况

  2. 工具调用脆弱:调用外部 API 时没有超时控制,有一次天气查询接口挂掉导致整个会话卡死 30 秒。更糟的是有些开发者会无脑重试,把错误次数也计入 prompt,结果 LLM 开始胡言乱语说 ” 系统正在第 5 次重试 …”

  3. 记忆模块简陋:所有对话记忆存在一个列表里,既没区分短期记忆(当前对话)和长期记忆(用户画像),也没有检索优化。测试时发现 Agent 总是引用 3 天前的旧地址,而用户明明刚更新过收货信息

模块化设计方案

经过几次迭代,我们总结出四层架构(附示意图):

graph TD
    A[Interface Layer] --> B[Orchestration Layer]
    B --> C[Memory Layer]
    B --> D[Tools Layer]

关键技术决策

  1. 记忆分层
  2. 短期记忆:固定窗口大小的对话历史(最近 10 轮)
  3. 长期记忆:向量数据库存储关键事实(用户偏好 / 业务规则)
  4. 元记忆:记录工具调用日志等系统级信息

  5. 工具熔断设计:当工具连续失败 3 次时,自动将其禁用 5 分钟,并通知运维系统。这里借鉴了 Circuit Breaker 模式:

class CircuitBreaker:
    def __init__(self, max_fails=3, cool_down=300):
        self._fails = 0
        self._last_fail = 0

    async def call(self, tool_func, *args):
        if time.time() - self._last_fail < self.cool_down:
            raise ToolTemporarilyDisabled()
        try:
            return await tool_func(*args)
        except Exception as e:
            self._fails += 1
            if self._fails >= self.max_fails:
                self._last_fail = time.time()
            raise

核心代码实现

带类型约束的 Agent 基类

from typing import Protocol, runtime_checkable

@runtime_checkable
class Tool(Protocol):
    name: str
    description: str

    async def __call__(self, input: str) -> str: ...

class AgentCore:
    def __init__(self, tools: list[Tool]):
        self._validate_tools(tools)  # 检查工具是否符合协议
        self.memory = VectorMemory()

    def _validate_tools(self, tools):
        for tool in tools:
            if not isinstance(tool, Tool):
                raise TypeError(f"工具 {tool.name} 未实现 Tool 协议")

记忆检索优化

class VectorMemory:
    def __init__(self):
        self._storage = []  # (vector, metadata)元组列表
        self._cache = LRUCache(maxsize=1000)

    def add_memory(self, text: str, importance: float):
        """
        参数 importance 控制记忆保留优先级
        0.5 为默认值,>0.8 的记忆会持久化到数据库
        """
        vector = get_embedding(text)
        self._storage.append((vector, {"text": text, "imp": importance}))

    def retrieve(self, query: str, top_k=3) -> list[str]:
        """
        使用余弦相似度过滤低相关性记忆
        对近期记忆给予时间衰减补偿
        """
        query_vec = get_embedding(query)
        scores = []
        for vec, meta in self._storage:
            sim = cosine_similarity(query_vec, vec)
            time_decay = 0.9 ** meta["age"]  # 每天衰减 10%
            scores.append((sim * time_decay * meta["imp"], meta["text"]))
        return [text for _, text in sorted(scores, reverse=True)[:top_k]]

生产环境验证

压力测试结果

使用 locust 模拟 100 并发用户时的表现:

指标 数值
平均响应延迟 320ms
95 分位延迟 890ms
错误率 0.12%

关键优化点:
– 对 LLM 的调用实现了请求批处理
– 向量检索改用 FAISS 加速
– 工具调用线程池限制最大并发数

安全防护措施

  1. 输入过滤
    def sanitize_input(text: str) -> str:
        # 移除可能破坏 prompt 的特殊符号
        return text.translate(str.maketrans({"<": "",">":"", "{": "","}":""}))
  2. 权限控制:每个工具注册时需要声明 required_scopes

避坑经验分享

记忆污染案例
曾经有用户问 ” 忘记密码怎么办 ”,Agent 回答步骤时,不小心把测试用的临时密码 ”123456″ 也包含在记忆里。之后所有密码相关提问都会引用这个错误值。现在我们采用以下策略:

  • 敏感信息检测(正则匹配手机号 / 密码等)
  • 重要事实需要用户二次确认才会存入长期记忆
  • 定期运行记忆清洗任务

工具权限清单示例

- name: process_refund
  scopes: [finance]
  rate_limit: 5/ 分钟
- name: query_weather
  scopes: [public]
  timeout: 2 秒

延伸思考

遇到一个有趣问题:当多个 Agent 需要协作时(比如销售 Agent 和技术支持 Agent 同时服务客户),如何解决这些场景:
1. 两个 Agent 同时调用支付接口
2. 对同一问题给出矛盾回答
3. 资源竞争(如同时请求语音合成服务)

我们搭建了一个测试沙盒,包含三个预配置 Agent:

POST /api/chat/multi-agent
{"agents": ["sales", "support", "translator"],
  "message": "这个价格能否包含安装服务?"
}

欢迎读者尝试后分享解决方案,目前我们采用的冲突解决策略是:
– 操作冲突:分布式锁 + 事务日志
– 回答冲突:基于 Agent 角色权重的投票机制
– 资源竞争:优先级队列 + 预分配配额

这套架构已在电商客服场景运行 6 个月,成功将平均问题解决轮次从 4.3 降低到 2.1。最大的收获是:Agent 系统不是越复杂越好,清晰的边界定义比算法优化更重要。

正文完
 0
评论(没有评论)