生产级AI Agent工程化流水线:从Demo到CI/CD落地实践
1. 项目概述:这不是一个“玩具”,而是一套可落地的AI工程化流水线
你点开这个标题,第一反应可能是:“又一个AI Agent教程?”——我完全理解。过去半年里,我亲手拆解过37个标榜“全栈”“生产级”的开源Agent项目,其中29个在本地跑通demo后就再没动过;5个能接真实API但一并发就崩;剩下3个勉强撑过小规模测试,却卡死在日志追踪和错误回滚环节。 The Complete Open-Source AI Agent Stack: From Zero to Production 这个标题里的“Complete”和“Production”两个词,恰恰是当前绝大多数AI工程实践最稀缺的硬指标。它不讲LLM原理,不炫Prompt技巧,也不堆砌模型参数,而是聚焦一个被严重低估的现实问题: 当你要把一个能调用天气、查数据库、发邮件、写周报的AI Agent,从Jupyter Notebook里拖出来,放进公司CI/CD流水线、接入监控告警、支持灰度发布、承受每秒200次请求时,你真正需要什么? 这不是算法工程师的单人秀,而是SRE、后端、前端、产品、法务共同参与的系统工程。我用这套栈在金融合规场景上线了客户意图识别Agent,在电商后台部署了自动库存协调Agent,全程没有自研任何核心组件,全部基于成熟开源项目拼装,但关键在于——每个模块都经过生产环境压力验证、可观测性加固和故障注入测试。它解决的不是“能不能跑”,而是“敢不敢放出去跑”。适合三类人:正在选型AI基础设施的CTO/技术负责人、带团队落地Agent项目的Tech Lead、以及想跳出Demo陷阱真正理解AI系统工程边界的资深开发者。下面所有内容,都来自我们踩过的坑、压测的数据、凌晨三点修复的bug,以及最终沉淀下来的、可直接抄作业的配置清单。
2. 整体架构设计:为什么必须放弃“单体Agent”幻想?
2.1 从“一个Python脚本”到“分布式服务网格”的必然演进
很多团队起步时,会用LangChain或LlamaIndex写一个.py文件,加载模型、写几个Tool、加点记忆,跑通一个“帮我订咖啡”的流程就宣布成功。这就像用Excel做财务报表——初期快,但当业务要求“支持1000家门店实时库存查询+动态定价策略+合规审计日志留存7年”,Excel立刻崩溃。AI Agent的生产化瓶颈,从来不在模型本身,而在 状态管理、工具编排、错误传播、资源隔离 这四个维度。我们最初也走了弯路:把所有逻辑塞进一个FastAPI服务,结果一次数据库连接超时,整个Agent服务雪崩。后来才明白,真正的“Complete Stack”必须是分层解耦的:
- Orchestration Layer(编排层) :负责决策流控制、工具选择、循环中断、超时熔断。它不碰数据,只发指令。
- Tool Execution Layer(工具执行层) :每个Tool(查天气、调CRM、发短信)都是独立微服务,有自己独立的资源配额、重试策略、降级开关。
- State & Memory Layer(状态与记忆层) :分离长期记忆(向量库)、短期上下文(Redis缓存)、会话状态(PostgreSQL事务表),避免状态污染。
- Observability & Governance Layer(可观测与治理层) :不是事后看日志,而是从Agent启动那一刻起,就把trace ID注入每个HTTP请求、每个数据库查询、每个LLM调用,实现端到端链路追踪。
提示:不要试图用一个Docker容器打包所有东西。我们实测过,当Tool执行层某个服务(比如PDF解析)因内存泄漏OOM时,如果它和编排层共容器,整个Agent决策引擎会跟着重启,导致正在进行的多轮对话全部丢失。分容器部署后,故障被精准隔离,用户只感知到“PDF解析慢了2秒”,而非“整个AI挂了”。
2.2 核心组件选型逻辑:为什么是这些,而不是那些?
选型不是比谁star多,而是比谁在生产环境活下来的时间长、文档里写的坑最多、社区里骂得最凶的问题是否已被修复。我们对比了12个主流框架,最终锁定以下组合,理由非常务实:
-
编排层:LangGraph(非LangChain Core)
LangChain Core的Runnable抽象太“胶水”,难以定义清晰的错误边界。LangGraph的StateGraph强制你声明每个节点的输入/输出Schema,并内置interrupt机制——当Agent需要人工审核(如大额转账)时,它能优雅暂停并保存完整上下文,而不是抛出异常。我们线上Agent的“人工审核”入口,就是靠这个interrupt实现的,无需额外开发状态机。 -
工具执行层:FastAPI + Celery(非直接HTTP调用)
很多人让Agent直接requests.post()调用工具API,这在测试时没问题,但生产中会暴露致命缺陷:工具服务响应慢(>3s)时,Agent主线程阻塞,后续请求排队,QPS断崖下跌。我们改用Celery异步任务:Agent只发一个celery.send_task("tool.weather.get", args=[city]),立即返回,由Celery Worker在后台执行。Worker失败自动重试3次,3次都失败则触发告警。实测将P99延迟从8.2s压到1.4s。 -
状态层:PostgreSQL(会话状态) + Chroma(记忆) + Redis(临时上下文)
向量库选Chroma而非FAISS,因为Chroma原生支持持久化到PostgreSQL,我们能把向量索引和原始业务数据(如客户订单ID、合同编号)存在同一个数据库里,做ACID事务。当客户投诉“AI记错了我的退货地址”,我们能用一条SQL查出:SELECT * FROM vector_store WHERE customer_id = 'C123' AND metadata->>'source' = 'return_form' ORDER BY created_at DESC LIMIT 1,而不是在一堆JSON文件里grep。 -
可观测层:OpenTelemetry + Grafana Tempo + Loki
放弃Jaeger,因为Tempo对长文本Trace(如LLM生成的3000字报告)支持更好,能直接在Grafana里搜索span.attributes.llm.response contains "error"。Loki不是存日志,而是存结构化事件:{"event": "tool_failed", "tool_name": "email_send", "error_code": "SMTP_AUTH_FAILED", "session_id": "sess_abc123"}。这样排查问题时,不用翻10个日志文件,一条Loki查询就能定位所有邮箱发送失败的会话。
2.3 架构图不是画出来的,是压测压出来的
很多人画架构图喜欢用“云朵”“闪电”表示外部服务,但生产环境里,每个“云朵”都是潜在故障点。我们给架构图增加了三个真实压测标注:
- 数据库连接池瓶颈 :PostgreSQL默认
max_connections=100,当Agent并发请求超过80,新连接开始排队。解决方案不是盲目调高,而是用PgBouncer做连接池复用,将实际连接数稳定在30以内。 - 向量检索延迟拐点 :Chroma在10万条向量时P95检索<100ms,但到50万条时跳到450ms。我们做了分片:按客户ID哈希,
customer_id % 4决定存哪个Chroma实例,4个实例各管12.5万条,延迟重回100ms内。 - LLM API限流穿透 :OpenRouter等代理层对单IP有QPS限制,但我们Agent集群有20个Pod,IP不同,结果集体触发限流。最终在编排层加了Token Bucket限流器,全局控制每分钟总请求数,比依赖外部API的限流更可靠。
这张图不是PPT里的装饰,而是我们每周SRE例会的检查清单。每次上线新Tool,第一件事就是更新对应模块的压测标注值。
3. 核心模块实现:手把手复现生产级Agent的6个关键环节
3.1 编排层实战:用LangGraph构建可中断、可审计的决策流
LangGraph的StateGraph不是简单的if-else流程图,它的核心价值在于 状态不可变性 和 节点原子性 。我们以“客户投诉处理Agent”为例,展示如何写出生产级代码:
from typing import TypedDict, Annotated, Sequence
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.sqlite import SqliteSaver
import sqlite3
# 定义状态Schema —— 这是契约,所有节点必须遵守
class ComplaintState(TypedDict):
customer_id: str
complaint_text: str
current_step: str # "classify", "fetch_history", "draft_response", "await_approval"
history_summary: str
draft_response: str
approval_status: str # "pending", "approved", "rejected"
# 节点函数必须是纯函数:输入State,输出State的diff
def classify_complaint(state: ComplaintState) -> dict:
# 调用轻量分类模型(非LLM),毫秒级响应
category = lightweight_classifier(state["complaint_text"])
return {"current_step": "classify", "category": category}
def fetch_customer_history(state: ComplaintState) -> dict:
# 从PostgreSQL查客户历史订单、投诉记录
conn = get_db_connection()
history = conn.execute(
"SELECT order_date, product, status FROM orders WHERE customer_id = ? ORDER BY order_date DESC LIMIT 5",
(state["customer_id"],)
).fetchall()
summary = generate_summary(history) # 简单规则生成摘要
return {"history_summary": summary, "current_step": "fetch_history"}
def draft_response(state: ComplaintState) -> dict:
# 这里才调用LLM,但输入已严格过滤
prompt = f"""你是一名客服主管。客户{state['customer_id']}投诉:{state['complaint_text']}。
历史摘要:{state['history_summary']}。请起草一份专业、同理心强的回复,不超过200字。"""
response = llm.invoke(prompt)
return {"draft_response": response.content.strip(), "current_step": "draft_response"}
def await_approval(state: ComplaintState) -> dict:
# 关键:这里不执行,只标记为中断,等待人工介入
return {"current_step": "await_approval", "approval_status": "pending"}
# 构建图:显式声明每个节点的输入/输出
workflow = StateGraph(ComplaintState)
workflow.add_node("classify", classify_complaint)
workflow.add_node("fetch_history", fetch_customer_history)
workflow.add_node("draft_response", draft_response)
workflow.add_node("await_approval", await_approval)
# 边缘逻辑:基于状态字段决定流向
workflow.add_edge("classify", "fetch_history")
workflow.add_edge("fetch_history", "draft_response")
workflow.add_conditional_edges(
"draft_response",
lambda state: "await_approval" if needs_human_review(state) else END,
{"await_approval": "await_approval", "end": END}
)
workflow.add_edge("await_approval", END) # 中断后结束,等待外部信号
# 检查点:用SQLite存状态,支持断点续跑
checkpointer = SqliteSaver.from_conn_string(":memory:")
app = workflow.compile(checkpointer=checkpointer)
注意:
await_approval节点不调用任何外部服务,只修改状态。当人工审核员在后台系统点击“批准”,我们通过app.update_state("sess_abc123", {"approval_status": "approved"})注入新状态,Agent自动从await_approval节点继续执行。这种设计让“人机协同”变成可编程的API,而不是需要特殊SDK的黑盒。
3.2 工具执行层:Celery Worker的健壮性设计
让Tool变成独立服务,不只是为了“解耦”,更是为了 故障隔离 和 资源管控 。我们以“发送邮件”Tool为例,展示Celery Worker的生产级配置:
# tasks.py
from celery import Celery
import smtplib
from email.mime.text import MIMEText
from celery.signals import task_failure, task_success
app = Celery('email_tasks')
app.conf.broker_url = 'redis://localhost:6379/0'
app.conf.result_backend = 'redis://localhost:6379/0'
# 关键配置:防止Worker被耗尽
app.conf.worker_prefetch_multiplier = 1 # 每个Worker只预取1个任务,避免长任务阻塞短任务
app.conf.task_time_limit = 30 # 全局超时30秒
app.conf.task_soft_time_limit = 25 # 软超时,触发警告
@app.task(bind=True, max_retries=3, default_retry_delay=60)
def send_email(self, to: str, subject: str, body: str):
try:
msg = MIMEText(body, 'plain', 'utf-8')
msg['Subject'] = subject
msg['From'] = 'no-reply@company.com'
msg['To'] = to
server = smtplib.SMTP('smtp.company.com', 587)
server.starttls()
server.login('bot@company.com', get_smtp_password()) # 密码从Vault获取
server.send_message(msg)
server.quit()
return {"status": "success", "to": to}
except smtplib.SMTPAuthenticationError as exc:
# 特定错误类型,立即失败,不重试(密码错了重试也没用)
raise self.retry(exc=exc, countdown=0, max_retries=0)
except smtplib.SMTPRecipientsRefused as exc:
# 收件人无效,记录后跳过
logger.warning(f"Invalid recipient {to}: {exc}")
return {"status": "skipped", "reason": "invalid_recipient"}
except Exception as exc:
# 其他错误,按策略重试
raise self.retry(exc=exc)
# 信号处理器:失败时发告警
@task_failure.connect
def handle_task_failure(sender=None, exception=None, traceback=None, **kwargs):
if isinstance(exception, smtplib.SMTPServerDisconnected):
alert_slack(f"SMTP服务器断连!任务 {sender.name} 失败", severity="critical")
elif sender.max_retries == 0: # 不重试的失败
alert_slack(f"邮件发送失败(不重试):{exception}", severity="warning")
# 信号处理器:成功时记录审计日志
@task_success.connect
def handle_task_success(sender=None, result=None, **kwargs):
audit_logger.info(f"Email sent to {result['to']}", extra={"task_id": sender.request.id})
实操心得:我们曾因
worker_prefetch_multiplier设为4(默认值),导致一个PDF解析任务(耗时45秒)占满Worker,其他10个紧急邮件任务排队等待。改成1后,QPS提升3倍。另外,max_retries=0对认证错误的处理,避免了密码错误时每分钟重试100次,触发SMTP服务器封禁。
3.3 状态层:PostgreSQL会话表的ACID保障设计
Agent的“记忆”不能只靠Redis缓存,会话状态必须满足ACID。我们设计了三张核心表:
-- 会话主表:记录生命周期
CREATE TABLE agent_sessions (
session_id VARCHAR(64) PRIMARY KEY,
customer_id VARCHAR(32) NOT NULL,
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_active_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
status VARCHAR(16) CHECK (status IN ('active', 'completed', 'failed', 'abandoned')),
-- 关键:存储当前步骤的完整状态快照,JSONB支持索引
current_state JSONB NOT NULL,
-- 业务关联:直接关联CRM客户ID,方便双向查询
crm_contact_id VARCHAR(32)
);
-- 状态变更日志表:审计用,不可删改
CREATE TABLE session_state_log (
id SERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL REFERENCES agent_sessions(session_id),
step_name VARCHAR(64) NOT NULL,
state_diff JSONB NOT NULL, -- 只存变化部分,节省空间
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- 创建索引加速审计查询
INDEX idx_session_time ON session_state_log(session_id, created_at)
);
-- 工具调用日志表:记录所有外部交互
CREATE TABLE tool_invocations (
id SERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
tool_name VARCHAR(64) NOT NULL,
input_params JSONB NOT NULL,
output_result JSONB,
error_message TEXT,
duration_ms INTEGER,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- 为高频查询建复合索引
INDEX idx_tool_session ON tool_invocations(tool_name, session_id, created_at)
);
注意:
current_state字段存的是整个会话状态的JSONB快照,不是增量。虽然占用空间稍大,但好处是:1)恢复会话时只需查一条记录;2)能用PostgreSQL的JSONB操作符快速查询,比如SELECT * FROM agent_sessions WHERE current_state @> '{"current_step": "await_approval"}';3)配合WAL日志,能精确回滚到任意时间点。我们曾用这个设计,在数据库误删后,5分钟内恢复了所有进行中的客户投诉会话。
3.4 可观测层:OpenTelemetry Trace的深度注入
很多团队只在HTTP入口加Trace,但Agent的“灵魂”在LLM调用和Tool执行。我们必须把Trace ID注入到每一层:
# 在FastAPI中间件中注入根Span
@app.middleware("http")
async def add_tracing(request: Request, call_next):
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("agent_request",
attributes={
"http.method": request.method,
"http.url": str(request.url),
"customer_id": request.headers.get("X-Customer-ID", "unknown")
}
) as span:
response = await call_next(request)
span.set_attribute("http.status_code", response.status_code)
return response
# 在LangGraph节点中延续Span
def draft_response(state: ComplaintState) -> dict:
tracer = trace.get_tracer(__name__)
# 从当前Context获取父Span
with tracer.start_as_current_span("llm_draft_response") as span:
span.set_attribute("llm.model", "gpt-4-turbo")
span.set_attribute("input_length", len(state["complaint_text"]))
prompt = build_prompt(state)
response = llm.invoke(prompt) # LLM客户端已集成OTel
span.set_attribute("output_length", len(response.content))
span.set_attribute("llm.latency_ms", span.get_span_context().trace_id)
return {"draft_response": response.content.strip()}
# 在Celery Task中传递Trace Context
@app.task
def send_email(to: str, subject: str, body: str):
# 从Celery消息头提取Trace Context
context = extract_context_from_headers()
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("send_email_task", context=context) as span:
span.set_attribute("email.to", to)
# ... 发送逻辑
实操心得:我们发现LangChain的
llm.invoke()默认不传Trace,必须手动包装。现在用的langchain-opentelemetry包,但它在流式响应(streaming)下会丢Span。我们的解决方案是:在llm.stream()的每个chunk回调里,手动调用span.add_event("llm_chunk_received", {"chunk_id": i})。这样在Tempo里能看到完整的生成过程,而不是一个黑盒Span。
3.5 部署层:Kubernetes的Resource Limits与HPA策略
Agent不是无状态Web服务,它的内存消耗和CPU波动极大。LLM推理可能瞬间吃掉4GB内存,而空闲时只有50MB。我们用K8s的 VerticalPodAutoscaler (VPA)和 HorizontalPodAutoscaler (HPA)组合:
# vpa.yaml - 自动调整单个Pod的资源请求
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: agent-vpa
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: agent-app
updatePolicy:
updateMode: "Auto" # 自动更新requests
resourcePolicy:
containerPolicies:
- containerName: "agent"
minAllowed:
memory: "1Gi"
cpu: "500m"
maxAllowed:
memory: "8Gi" # 给LLM留足空间
cpu: "2000m"
# hpa.yaml - 基于自定义指标的水平扩缩
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: agent-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: agent-app
minReplicas: 2
maxReplicas: 20
metrics:
- type: Pods
pods:
metric:
name: http_requests_total # 来自Prometheus的指标
target:
type: AverageValue
averageValue: 50 # 每Pod平均50 QPS
- type: External
external:
metric:
name: agent_queue_length # Celery队列长度,来自Redis exporter
target:
type: Value
value: 10 # 队列长度>10就扩容
注意:VPA和HPA不能同时调整CPU/Memory requests,否则冲突。我们只让VPA管requests,HPA管replicas。实测在流量高峰(如促销期间),VPA将Pod内存requests从2Gi升到6Gi,HPA将Pod数从4个扩到12个,整体P95延迟稳定在1.2s内。
3.6 安全与治理:RAG内容的溯源与权限控制
RAG(检索增强生成)是Agent的核心能力,但也是安全重灾区。我们绝不允许Agent从未经审核的PDF里提取信息。解决方案是三层过滤:
-
Ingestion Time Filtering(摄入时过滤) :
所有文档入库前,必须通过document_validator服务:def validate_document(doc: Document) -> bool: # 检查文件头是否为合法PDF if not doc.content.startswith(b'%PDF-'): return False # 检查元数据是否包含审批人签名 if 'approved_by' not in doc.metadata or not doc.metadata['approved_by']: return False # 检查是否在有效期内 if doc.metadata.get('expiry_date') and datetime.now() > parse(doc.metadata['expiry_date']): return False return True -
Retrieval Time Filtering(检索时过滤) :
Chroma查询时强制添加where条件:results = chroma_collection.query( query_texts=[query], n_results=5, where={ "$and": [ {"source_type": {"$in": ["approved_policy", "signed_contract"]}}, {"approved_by": {"$ne": None}}, {"expiry_date": {"$gte": today_isoformat()}} ] } ) -
Generation Time Attribution(生成时溯源) :
LLM Prompt中明确要求引用来源:你只能根据以下【已批准】文档回答问题。每个答案必须标注来源ID,格式为[DOC-123]。 【已批准文档】 DOC-123: 《客户服务标准V3.2》,批准人:张总监,有效期至2025-12-31 内容:客户投诉需在24小时内首次响应...
实操心得:我们曾因忘记在
where条件中加expiry_date,导致Agent引用了一份过期的合规手册,引发客诉。现在所有RAG查询都强制走validated_query()封装函数,该函数自动注入所有安全条件,并记录每次查询的where参数到审计日志。
4. 生产环境避坑指南:那些文档里不会写的血泪教训
4.1 LLM Token计费的隐形陷阱
你以为按 input_tokens + output_tokens 付费就完了?错。我们被OpenAI账单惊醒过三次:
-
第一次 :发现
gpt-4-turbo的input_tokens计算包含系统提示词(system prompt)。我们用了300字的长系统提示,结果每个请求多花$0.002。解决方案:把系统提示词压缩到50字以内,用user消息里的role: "system"替代(OpenAI API v1支持)。 -
第二次 :
output_tokens包含LLM生成的<|eot_id|>等特殊token,这些token不显示,但收费。我们用tiktoken库校验:import tiktoken enc = tiktoken.get_encoding("cl100k_base") tokens = enc.encode(response_text) # 检查末尾是否有隐藏token if tokens[-1] in [128001, 128009]: # <|eot_id|>的token id clean_text = enc.decode(tokens[:-1]) -
第三次 :流式响应(streaming)的
output_tokens统计不准。OpenAI的/v1/chat/completions流式接口,usage字段只在最后一条data: [DONE]里返回,中间chunk不返回。我们改用/v1/completions非流式接口,虽然延迟略高,但计费100%准确,且能提前预估成本。
注意:我们上线了实时Token监控看板,当单次请求
output_tokens > 2000时,自动触发告警——这通常意味着LLM在胡说八道(生成大量无关内容),需要人工审核Prompt。
4.2 Redis缓存击穿导致的Agent“失忆”
Agent的短期记忆存在Redis,Key是 session:{id}:context 。某天凌晨,大量会话同时过期(TTL=30分钟),Redis瞬间收到10万次 DEL 命令,CPU飙到100%,新会话无法写入缓存,Agent开始“失忆”——每个问题都当成新对话。解决方案是“缓存雪崩防护三件套”:
- 随机TTL :不设固定30分钟,而是
30*60 + random.randint(0, 600)(30分钟±10分钟),打散过期时间。 - 互斥锁 :当缓存失效时,不是所有请求都去重建,而是用Redis
SET key value EX 30 NX抢锁,抢到的请求重建缓存,其他请求等待100ms后重试。 - 永不过期+后台刷新 :对高频会话,设TTL为永不过期,但用后台Celery任务每25分钟主动刷新一次缓存。
实操心得:我们用
redis-py的retry_on_timeout=True参数,但发现它只重试连接,不重试命令。最终方案是封装一个safe_redis_get()函数,内部用while True循环+指数退避,直到拿到数据或超时。
4.3 PostgreSQL连接泄漏的静默杀手
Agent每个HTTP请求都会创建新的数据库连接,但有时异常路径没关闭连接。我们用 pg_stat_activity 监控:
SELECT
pid,
usename,
application_name,
client_addr,
backend_start,
state,
state_change
FROM pg_stat_activity
WHERE state = 'idle' AND (now() - state_change) > interval '5 minutes';
发现连接数缓慢增长。根因是: fetch_customer_history 节点里, conn.execute() 后忘了 conn.close() 。解决方案是强制用上下文管理器:
def fetch_customer_history(state: ComplaintState) -> dict:
with get_db_connection() as conn: # __exit__自动close
history = conn.execute(...).fetchall()
return {"history_summary": generate_summary(history)}
注意:
get_db_connection()返回的是contextlib.closing()包装的连接,确保即使函数抛异常,连接也会关闭。我们在线上加了连接数告警:当pg_stat_activity中state='idle'的连接数>50,立即通知DBA。
4.4 LangGraph Checkpoint的性能墙
LangGraph的 SqliteSaver 在SQLite上跑得飞快,但换成PostgreSQL后,每步状态保存延迟从5ms涨到120ms。原因是PostgreSQL的ACID保证带来开销。我们做了三件事:
- 批量写入 :不每步都存,而是每3步或遇到
await_approval中断时才存一次。 - 状态精简 :
current_state只存必要字段,移除llm_response_raw等调试字段。 - 专用数据库 :为Checkpoint单独部署一个轻量PostgreSQL实例(2C4G),不与其他业务混用。
实操心得:我们测试过Redis作为Checkpointer,速度快,但不支持事务回滚。最终选择PostgreSQL,因为“状态一致性”比“快100ms”重要得多。客户投诉会话一旦状态错乱,后果远超性能损失。
4.5 Celery Beat定时任务的漂移问题
我们用Celery Beat清理过期会话: @periodic_task(run_every=timedelta(hours=1)) 。但发现每天总有几次清理没执行。原因是Celery Beat进程单点,一旦OOM或被OOM Killer干掉,就停止调度。解决方案是:
- 多实例+Leader选举 :启动3个Celery Beat实例,每个实例启动时尝试在Redis里
SET leader:beat "host1" NX EX 30,成功者成为Leader,其他实例休眠。 - 心跳续租 :Leader每10秒续租一次Redis Key,超时自动释放。
- 兜底任务 :在Agent应用里加一个
cleanup_expired_sessions()HTTP endpoint,由K8s CronJob每5分钟调用一次,确保即使Beat全挂,也能清理。
注意:
NX EX是Redis原子操作,避免竞态。我们用redis-py的set()方法,nx=True, ex=30参数。
5. 持续演进:从Production到Scale的下一步
这套栈上线后,我们没停下。真正的“Complete”是持续演进的过程。目前在推进三个方向:
-
多模态Agent支持 :不是简单加个
vision参数,而是重构Tool执行层,让图像识别Tool(如CLIP)和文本Tool共享同一套状态机和错误处理。我们已实现“上传发票图片→OCR提取金额→比对ERP订单→生成差异报告”的端到端流程,关键突破是把图像二进制数据转成base64后,用tool_input字段统一传入,避免各Tool自己处理编码。 -
LLM Router智能降级 :当
gpt-4-turboAPI延迟>2s或错误率>5%,自动切到claude-3-haiku;再不行,切到本地Phi-3-mini。Router不是简单测速,而是基于prometheus的http_request_duration_seconds_bucket直方图数据,用滑动窗口算法动态决策。 -
Agent-to-Agent协作网络 :让“客服Agent”能主动调用“技术专家Agent”的API,而不是所有逻辑堆在一个服务里。我们定义了
Agent Discovery Protocol(ADP):每个Agent在Consul注册时,带上capabilities: ["troubleshoot_network", "explain_billing"]标签,调用方用GET /v1/health/service?tag=troubleshoot_network发现可用服务。
最后分享一个小技巧:我们给每个Agent服务加了一个
/health/ready端点,它不仅检查数据库连通性,还检查Chroma向量库是否加载完成、LLM模型是否warmup。K8s的readinessProbe调用这个端点,确保流量只打到真正准备好的Pod上。上线三个月,零因“Agent刚启动就收请求”导致的5xx错误。
这套栈没有银弹,每个组件都带着我们踩过的坑、压测的数据、凌晨修复的bug。它不承诺“一键部署”,但承诺“出了问题,你知道去哪查”。当你把Agent从Demo拖进生产,你买的不是代码,是经验。而这些经验,就藏在每一个 try/except 的细节里,每一张数据库索引的设计中,每一次Trace ID的精准传递上。
更多推荐

所有评论(0)