Declarai+FastAPI+Streamlit:声明式LLM应用开发实战

📅 2026/7/12 5:50:19
Declarai+FastAPI+Streamlit:声明式LLM应用开发实战
1. 项目概述为什么一个“声明式LLM应用框架”值得你花20分钟读完这篇实操笔记我第一次在GitHub上看到Declarai这个库时心里是存疑的——又一个包装LLM调用的Python库但当我用它3分钟内把一个带记忆、带工具调用、带结构化输出的Chatbot从零搭出来且全程没写一行requests.post()或openai.ChatCompletion.create()我才意识到这不是语法糖而是一次LLM工程范式的微小但真实的位移。Declarai的核心价值不在于它多快或多炫而在于它把“让大模型按你写的Python函数签名来做事”这件事变成了可声明、可测试、可版本化、可嵌入任何后端或前端的确定性行为。它和FastAPI、Streamlit不是简单拼凑而是形成了一条极简但完整的LLM应用交付链路Declarai负责语义契约层你定义“这个函数该返回什么格式”它确保大模型真这么干FastAPI负责服务契约层你定义HTTP接口长什么样它自动生成OpenAPI文档和校验逻辑Streamlit负责交互契约层你用几行st.chat_message就能复现专业级对话UI。这三者组合真正解决了LLM应用开发中最消耗时间的三个痛点提示词调试反复改、API响应格式不可靠、前端交互逻辑和后端耦合太深。如果你正在做内部知识助手、客服话术生成器、合同条款提取工具或者任何需要“稳定输出结构化JSON自然语言混合响应”的场景这套组合拳比手写LangChain Chain或自己封装OpenAI SDK要省下至少60%的胶水代码时间。它不替代你对模型能力的理解但能让你把精力聚焦在“业务逻辑怎么编排”而不是“怎么让模型别乱加字段”。2. 核心技术栈解构Declarai不是魔法是把LLM当“强类型函数”来用2.1 Declarai的设计哲学从“提示词工程”到“函数契约工程”传统LLM调用方式比如直接调用OpenAI API本质是“弱类型”的你传一段字符串过去期望模型返回一段字符串中间没有任何编译期或运行期的类型保障。你得靠正则去extract JSON靠try-except去catch格式错误靠人工review日志去发现模型偷偷加了个字段。Declarai反其道而行之它强制你用Python的model装饰器和TypedDict/Pydantic BaseModel来定义一个“LLM函数”from declarai import Declarai from pydantic import BaseModel from typing import List class ProductSummary(BaseModel): name: str key_features: List[str] target_audience: str model( systemYou are a senior product marketing manager. Extract key information from product descriptions., modelopenai:gpt-4-turbo ) def summarize_product(description: str) - ProductSummary: Extract structured summary from raw product description text.这段代码里summarize_product不是一个普通函数而是一个LLM驱动的、强类型的、可调用的接口契约。Declarai在背后做了三件关键事第一自动把函数签名参数名、类型、docstring和model里的system prompt拼成一个高质量的提示词模板第二在调用时自动注入description的值并设置response_format{type: json_object}对支持的模型第三拿到响应后用Pydantic进行严格校验——如果模型返回了{name: X, features: [...]}注意字段名拼错了它会立刻抛出ValidationError而不是让你在下游业务逻辑里崩溃。这相当于给LLM加了一层TypeScript式的类型守门员。我实测过在处理1000条电商商品描述时手写prompt正则解析的失败率是7.3%而DeclaraiPydantic的失败率是0.2%全是模型彻底胡说八道的极端case校验层成功拦截。这不是玄学是工程确定性的胜利。2.2 FastAPI的精准定位不做LLM调度只做“契约执行器”很多人一上来就想用FastAPI去“管理模型实例”“做负载均衡”“写推理路由”这是典型的本末倒置。在这个技术栈里FastAPI的唯一职责是把Declarai定义的那些“LLM函数”变成标准、可靠、可文档化的HTTP接口。它的价值体现在三个不可替代的环节输入校验、输出序列化、OpenAPI自动化。看一个真实例子from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List app FastAPI(titleProduct Assistant API) class SummarizeRequest(BaseModel): description: str max_features: int 5 class SummarizeResponse(BaseModel): name: str key_features: List[str] target_audience: str confidence_score: float # 这个字段Declarai不生成我们后加的业务逻辑 app.post(/summarize, response_modelSummarizeResponse) def api_summarize(request: SummarizeRequest): try: # 直接调用Declarai函数像调用本地函数一样 result summarize_product( descriptionrequest.description, # 注意这里不能传max_features因为Declarai函数签名里没有它 ) # 后续可以加业务逻辑比如基于result.name查数据库打分 return SummarizeResponse( **result.dict(), confidence_score0.92 # 模拟业务计算 ) except ValidationError as e: raise HTTPException(422, fLLM output validation failed: {e})这里的关键洞察是FastAPI的request: SummarizeRequest完成了客户端输入的强校验比如description不能为空、max_features必须是int而response_modelSummarizeResponse则保证了返回给前端的JSON一定是符合Schema的。更妙的是当你访问/docs时FastAPI自动生成的Swagger UI里/summarize接口的请求体和响应体都带着清晰的字段说明、类型、示例连key_features是array of string都标得明明白白。这意味着你的前端工程师不用猜、不用问、不用看Python代码直接照着文档就能写调用逻辑。我合作过的三个前端团队反馈这种“契约先行”的API比他们之前对接的“口头约定型”LLM API联调时间平均缩短了80%。FastAPI在这里不是LLM引擎而是那个一丝不苟、拿着尺子量你每一寸输入输出的质检员。2.3 Streamlit的不可替代性用“声明式UI”对抗LLM的“非确定性”Streamlit常被误认为是“玩具级”工具但在LLM应用中它恰恰是最契合的前端框架。原因很简单LLM的输出天生是非确定性的同一输入可能有多个合理回答而Streamlit的st.session_state和st.chat_message天然支持“状态驱动”的对话流。对比一下传统React方案你需要手动管理messages数组、处理loading状态、防重复提交、滚动到底部……而Streamlit里核心对话循环就这12行import streamlit as st if messages not in st.session_state: st.session_state.messages [] for msg in st.session_state.messages: with st.chat_message(msg[role]): st.markdown(msg[content]) if prompt : st.chat_input(Ask about products...): st.session_state.messages.append({role: user, content: prompt}) with st.chat_message(user): st.markdown(prompt) with st.chat_message(assistant): message_placeholder st.empty() full_response # 这里调用FastAPI或直接调用Declarai response summarize_product(prompt) full_response response.json(indent2) # 或者渲染成更友好的markdown message_placeholder.markdown(full_response) st.session_state.messages.append({role: assistant, content: full_response})这段代码里没有useState、没有useEffect、没有axios调用只有“数据变了UI自动重绘”的纯粹声明式逻辑。st.session_state.messages就是你的整个对话历史st.chat_message就是你的消息气泡组件st.chat_input就是那个带回车提交的输入框。它不追求像素级定制但完美覆盖了90%的LLM对话场景。更重要的是Streamlit的st.cache_resource可以安全地缓存Declarai的Declarai实例它内部管理着模型连接池避免每次请求都重建连接。我在一个日均500次请求的内部工具中实测加上st.cache_resource后首字响应时间TTFT从平均1.8s降到0.9s提升几乎翻倍。这不是框架的功劳而是Streamlit对“状态”和“资源”的朴素而精准的抽象恰好踩中了LLM应用最需要的节奏。3. 实操全流程拆解从零搭建一个带记忆的客服问答Bot3.1 环境准备与依赖安装避开Python包冲突的三个深坑别跳过这一步。我见过太多人卡在环境配置上不是因为技术难而是因为几个隐蔽的依赖冲突。以下是经过生产验证的requirements.txt精简版仅保留核心去掉所有可选依赖# 核心框架 fastapi0.115.0 uvicorn[standard]0.32.0 streamlit1.39.0 declarai0.12.3 # 注意必须用0.12.x0.11.x有Pydantic v2兼容问题 # LLM后端按需选一个 openai1.54.2 # 如果用OpenAI # or anthropic0.42.0 # 如果用Claude # or google-generativeai0.8.4 # 如果用Gemini # 类型系统Declarai强依赖 pydantic2.9.2 typing-extensions4.12.2 # 其他必要工具 httpx0.27.2 # FastAPI底层HTTP客户端新版必须提示declarai0.12.3是当前最稳定的版本。如果你用pip install declarai默认会装0.13.x但它和pydantic2.9.2有已知的ValidationError序列化bug会导致FastAPI的response_model校验失败。务必显式指定版本。安装时绝对不要用pip install -r requirements.txt一次性装完。我的推荐流程是pip install --upgrade pip setuptools wheel先升级基础工具pip install pydantic2.9.2 typing-extensions4.12.2先装好类型系统基石pip install declarai0.12.3再装Declarai让它基于正确的Pydantic构建pip install fastapi uvicorn[standard] streamlit最后装框架pip install openai1.54.2按需装LLM后端为什么这样分步因为declarai的setup.py里会动态检查Pydantic版本如果先装了新版Pydantic它会尝试用v1的API去初始化导致后续model装饰器失效。我踩过这个坑在一台新Mac M2上折腾了3小时才定位到。现在我的CI脚本里这五步是原子化的少一步都不行。3.2 Declarai模型定义如何写出“一次写对、永不调试”的提示契约真正的生产力提升始于Declarai模型定义的质量。我总结出三条铁律每一条都来自线上事故的教训铁律一System Prompt必须包含“角色任务约束”三要素缺一不可错误写法model(systemAnswer questions about products) def answer_qa(question: str) - str: ...问题模型不知道自己是谁客服工程师、不知道任务边界只答已知信息还是可以推测、没有输出约束要简洁要分点。结果是模型自由发挥输出不稳定。正确写法model( system( You are a senior customer support agent for Acme Corp. Your task is to answer ONLY questions about our product catalog. If the question is outside product specs (e.g., company history, pricing), respond with I can only help with product features. Always respond in plain English, no markdown, no bullet points, under 100 words. ), modelopenai:gpt-4-turbo ) def support_qa(question: str) - str: Answer customer questions based on product knowledge base.这里“senior customer support agent”定义了角色“answer ONLY questions about our product catalog”定义了任务边界“respond with I can only help...”和“under 100 words”是硬性约束。实测表明加入明确约束后模型越界回答率从32%降到1.7%。铁律二输入参数名必须是业务语义名而非技术占位符错误写法def process(text: str) - dict: ... # text是什么用户问题产品描述日志正确写法def support_qa(customer_question: str, product_context: str ) - SupportAnswer: ...参数名customer_question和product_context本身就是文档。当其他工程师阅读代码时不需要看docstring就知道这个函数要什么。而且Declarai会把参数名自动注入到提示词里例如“Heres the customer_question: ...”这比你手动拼接字符串更可靠。铁律三输出类型必须用Pydantic BaseModel且每个字段都要有Field(description...)from pydantic import BaseModel, Field class SupportAnswer(BaseModel): answer: str Field( ..., descriptionThe direct, concise answer to the customers question. No preamble. ) confidence: float Field( ..., ge0.0, le1.0, descriptionA score from 0.0 (low confidence) to 1.0 (high confidence). ) requires_human_review: bool Field( defaultFalse, descriptionTrue if the answer touches on legal, safety, or unverified claims. )Field(description...)会被Declarai自动加入到提示词的“output format instructions”部分。比如它会告诉模型“You must output a JSON with keys answer, confidence, requires_human_review. The answer field must contain only the direct answer, no preamble.” 这比你在system prompt里写“请返回JSON”要精确10倍。我做过AB测试用Field(description)的模型输出合规率是99.4%纯靠system prompt指令的是87.1%。3.3 FastAPI服务层实现如何让LLM接口像银行API一样可靠一个健壮的LLM API必须处理三类典型失败模型超时、模型胡说、业务逻辑异常。下面是一个生产就绪的FastAPI路由实现包含了所有防御性编程from fastapi import FastAPI, HTTPException, status, BackgroundTasks from pydantic import BaseModel, Field from typing import Optional import logging import time from declarai.exceptions import DeclaraiError logger logging.getLogger(__name__) app FastAPI( titleAcme Support API, descriptionLLM-powered customer support assistant with memory and tool calling, version1.0.0 ) class SupportRequest(BaseModel): customer_question: str Field(..., min_length1, max_length2000) session_id: str Field(..., patternr^[a-zA-Z0-9_-]{8,64}$) # 强制合法session id product_context: Optional[str] Field(default, max_length5000) class SupportResponse(BaseModel): answer: str confidence: float requires_human_review: bool request_id: str # 用于日志追踪 latency_ms: int app.post(/v1/support, response_modelSupportResponse, tags[support]) async def handle_support_request( request: SupportRequest, background_tasks: BackgroundTasks ): start_time time.time() request_id freq_{int(start_time * 1000000)} # 简单唯一ID try: # Step 1: 输入预处理业务规则 if password in request.customer_question.lower(): logger.warning(f[{request_id}] Password-related question blocked) raise HTTPException( status_codestatus.HTTP_400_BAD_REQUEST, detailQuestions about passwords or credentials are not allowed. ) # Step 2: 调用Declarai模型核心LLM调用 # 注意这里我们假设已经有一个全局的declarai_instance # 在实际项目中它应该用Depends或Lifespan管理 result support_qa( customer_questionrequest.customer_question, product_contextrequest.product_context ) # Step 3: 业务后处理非LLM逻辑 # 例如基于confidence决定是否触发人工审核队列 if result.confidence 0.7: background_tasks.add_task( trigger_human_review, request_id, request.customer_question, result.answer ) # Step 4: 构建响应 latency_ms int((time.time() - start_time) * 1000) response SupportResponse( answerresult.answer, confidenceresult.confidence, requires_human_reviewresult.requires_human_review, request_idrequest_id, latency_mslatency_ms ) logger.info(f[{request_id}] Success. Latency: {latency_ms}ms, Confidence: {result.confidence:.2f}) return response except DeclaraiError as e: # DeclaraiError是所有LLM调用失败的基类网络、token超限、模型拒绝等 logger.error(f[{request_id}] DeclaraiError: {e}) raise HTTPException( status_codestatus.HTTP_503_SERVICE_UNAVAILABLE, detailfLLM service temporarily unavailable: {str(e)[:100]} ) except ValidationError as e: # Pydantic校验失败说明模型输出严重违规 logger.error(f[{request_id}] ValidationError: {e}) raise HTTPException( status_codestatus.HTTP_500_INTERNAL_SERVER_ERROR, detailLLM returned invalid response format. Please try again. ) except Exception as e: # 兜底异常如数据库查询失败等 logger.critical(f[{request_id}] Unexpected error: {e}, exc_infoTrue) raise HTTPException( status_codestatus.HTTP_500_INTERNAL_SERVER_ERROR, detailAn unexpected error occurred. ) # 后台任务人工审核触发模拟 async def trigger_human_review(request_id: str, question: str, answer: str): # 这里可以发消息到Slack、写入数据库、调用CRM API等 await asyncio.sleep(0.1) # 模拟异步操作 logger.info(f[{request_id}] Human review triggered for low-confidence answer)这个实现的关键细节session_id的正则校验防止恶意用户传入超长字符串或SQL注入payloadpatternr^[a-zA-Z0-9_-]{8,64}$是经过安全审计的宽松模式。敏感词拦截在LLM调用前就做业务规则过滤避免把“密码”“银行卡”这类词传给模型既安全又省Token。background_tasks的使用把耗时的、非关键路径的业务逻辑如发通知、记日志放到后台保证主响应路径的低延迟。分层异常处理DeclaraiErrorLLM层、ValidationError契约层、Exception兜底层分别对应不同HTTP状态码前端可以精准区分是“服务挂了”还是“模型瞎说了”还是“我们代码崩了”。部署时我用uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 --timeout-keep-alive 60启动4个worker足以应对100QPS的内部流量--timeout-keep-alive 60是为了避免Streamlit频繁短连接导致的TIME_WAIT堆积。3.4 Streamlit前端集成如何用15行代码做出专业级对话体验Streamlit的魔力在于它把“状态管理”这个前端最头疼的问题降维成了Python变量。下面是一个完整、可运行的app.py它实现了带历史记录、带加载状态、带错误提示的客服Botimport streamlit as st import httpx # 推荐用httpx比requests更现代支持异步 import json # 配置 API_BASE_URL http://localhost:8000 # 本地开发时指向FastAPI # API_BASE_URL https://your-prod-api.com # 生产时换这里 # 初始化session state if messages not in st.session_state: st.session_state.messages [ {role: assistant, content: Hello! Im Acme Support Bot. How can I help you with our products today?} ] if session_id not in st.session_state: st.session_state.session_id sess_ str(hash(time.time()))[:8] # 页面标题 st.title( Acme Product Support) st.caption(Powered by Declarai FastAPI Streamlit) # 显示历史消息 for msg in st.session_state.messages: with st.chat_message(msg[role]): st.markdown(msg[content]) # 处理用户输入 if prompt : st.chat_input(Ask about features, compatibility, or setup...): # 添加用户消息到历史 st.session_state.messages.append({role: user, content: prompt}) with st.chat_message(user): st.markdown(prompt) # 调用FastAPI后端 with st.chat_message(assistant): message_placeholder st.empty() with st.spinner(Thinking...): try: response httpx.post( f{API_BASE_URL}/v1/support, json{ customer_question: prompt, session_id: st.session_state.session_id, # product_context 可以从知识库动态注入此处简化 }, timeout30.0 ) response.raise_for_status() data response.json() # 渲染结构化响应 answer data[answer] confidence data[confidence] needs_review data[requires_human_review] # 构建富文本响应 display_text f{answer}\n\n*Confidence: {confidence:.2%}* if needs_review: display_text \n\n⚠️ This answer has been flagged for human review. message_placeholder.markdown(display_text) st.session_state.messages.append({role: assistant, content: display_text}) except httpx.HTTPStatusError as e: error_msg f❌ API Error {e.response.status_code}: {e.response.json().get(detail, Unknown error)} message_placeholder.error(error_msg) st.session_state.messages.append({role: assistant, content: error_msg}) except httpx.TimeoutException: error_msg ❌ Request timed out. Please try again. message_placeholder.error(error_msg) st.session_state.messages.append({role: assistant, content: error_msg}) except Exception as e: error_msg f❌ Unexpected error: {str(e)} message_placeholder.error(error_msg) st.session_state.messages.append({role: assistant, content: error_msg})这个app.py的亮点st.chat_message的原生支持无需CSS hack消息气泡自动左右对齐、带头像图标、响应式布局。st.spinner和st.error的精准控制加载状态只包裹LLM调用段错误提示直接显示在气泡里用户感知清晰。结构化数据的友好渲染把confidence转成百分比{confidence:.2%}把requires_human_review转成醒目的⚠️图标信息密度高但不杂乱。错误分类处理HTTPStatusErrorAPI返回4xx/5xx、TimeoutException网络超时、Exception其他分别给出不同提示用户知道下一步该做什么。启动命令就是简单一句streamlit run app.py。它会自动打开浏览器你甚至不需要配Nginx反向代理——Streamlit内置的Tornado服务器足够应付内部工具需求。我把它部署在公司内网的一台4C8G的云主机上同时在线50人CPU占用常年低于30%。4. 常见问题与实战排错那些文档里不会写的血泪经验4.1 Declarai常见报错速查表错误现象根本原因解决方案我的实操心得DeclaraiError: Model openai:gpt-4-turbo not found环境变量OPENAI_API_KEY未设置或openai包版本不匹配检查echo $OPENAI_API_KEY确认pip list | grep openai输出是1.54.2在代码开头加print(API Key loaded:, bool(os.getenv(OPENAI_API_KEY)))不要相信文档说的“自动加载”。我遇到过三次都是因为.env文件没被python-dotenv正确读取最终发现是.env文件编码是UTF-8 with BOM改成纯UTF-8就解决了。ValidationError: 1 validation error for SupportAnswer\nanswer\n field required模型返回的JSON缺少answer字段或字段值为null在Declarai函数的model里加temperature0.1降低随机性在Pydantic模型里给字段加default或default_factorystrdefault_factorystr比default更安全因为是空字符串而str()是空字符串的构造函数能避免某些模型返回null时的校验失败。RuntimeError: Working outside of application context在FastAPI的BackgroundTasks里直接用了st.session_stateStreamlit专属绝对不要在FastAPI里导入streamlitBackgroundTasks只能处理纯Python逻辑。把st.session_state相关操作全部移到Streamlit前端。这是个经典陷阱。新手常想“在后台更新Streamlit状态”但Streamlit的状态是前端Session专属的后端根本无权访问。正确做法是后端只发事件如Webhook前端用st.experimental_rerun()刷新。4.2 FastAPI性能瓶颈排查为什么你的LLM API越来越慢现象刚上线时TTFT首字响应时间是800ms两周后变成2.5suvicorn日志里大量INFO: 127.0.0.1:54321 - POST /v1/support HTTP/1.1 200 OK但耗时很长。排查步骤确认是不是LLM本身变慢直接用curl绕过FastAPI调用Declarai的Python函数看耗时。如果也变慢问题在LLM后端如OpenAI API限流、网络抖动。确认是不是FastAPI中间件拖慢在main.py里临时注释掉所有app.middleware只留最简路由。如果恢复说明某个中间件如日志、鉴权有阻塞。最关键的一步检查declarai实例是否被重复创建。很多新手在每个请求里都写declarai_instance Declarai(...)这会导致每次请求都新建一个HTTP连接池连接数爆炸。正确做法是# ✅ 正确全局单例用Lifespan管理 from contextlib import asynccontextmanager from declarai import Declarai asynccontextmanager async def lifespan(app: FastAPI): # 启动时创建 app.state.declarai Declarai( provideropenai, modelgpt-4-turbo, api_keyos.getenv(OPENAI_API_KEY) ) yield # 关闭时清理可选 # await app.state.declarai.close() app FastAPI(lifespanlifespan) # 在路由里用 app.post(/v1/support) def support(request: SupportRequest): result app.state.declarai.support_qa(...) # 复用实例我修复过一个案例客户把Declarai实例放在路由函数里QPS 20时netstat -an \| grep :8000 \| wc -l显示ESTABLISHED连接数高达1200远超系统默认的1024上限导致新连接被拒绝。改成Lifespan单例后连接数稳定在20-30个。4.3 Streamlit部署黑盒为什么你的st.chat_message在服务器上不显示现象本地streamlit run app.py一切正常但部署到Linux服务器后页面空白或消息气泡不渲染浏览器控制台报WebSocket connection to ws://... failed。根本原因和解决方案问题1Streamlit默认只监听127.0.0.1。远程访问时WebSocket连接被拒绝。解决启动时加--server.address0.0.0.0 --server.port8501并确保防火墙放行8501端口。问题2反向代理Nginx/Apache未正确配置WebSocket头。解决Nginx配置片段location / { proxy_pass http://127.0.0.1:8501; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # 关键 proxy_set_header Connection upgrade; # 关键 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }问题3服务器内存不足Streamlit进程被OOM Killer杀死。解决dmesg -T \| grep -i killed process查看日志用free -h确认内存Streamlit默认吃内存加--server.maxUploadSize100限制上传大小或用--server.headlesstrue减少GUI开销。我遇到过最诡异的一次服务器是ARM64架构AWS Graviton而Streamlit的某些依赖如Pillow在ARM上编译有问题导致WebSocket握手失败。最终解决方案是pip uninstall pillow pip install --only-binaryall pillow强制用预编译二进制。5. 进阶扩展与生产就绪建议让这个Bot走出Demo走进业务线5.1 加入记忆用Redis实现跨会话上下文Declarai本身不管理状态但你可以轻松把它和Redis结合实现真正的“记住用户之前问过什么”。核心思路在FastAPI路由里根据session_id从Redis读取最近3轮对话作为product_context注入到Declarai调用中。import redis from redis import Redis # 初始化Redis客户端生产环境用连接池 redis_client Redis(hostlocalhost, port6379, db0, decode_responsesTrue) def get_session_history(session_id: str, limit: int 3) - str: 从Redis读取session历史格式化为字符串 key fsession:{session_id}:history # Redis list最新消息在左边 messages redis_client.lrange(key, 0, limit-1) if not messages: return # 格式化为User: ...\nAssistant: ... formatted [] for i, msg in enumerate(messages): role, content msg.split(:, 1) if : in msg else (Unknown, msg) prefix Customer if role.strip().lower() user else Assistant formatted.append(f{prefix}: {content.strip()}) return \n.join(formatted) def save_to_session_history(session_id: str, role: str, content: str): 保存消息到Redis保持最新3条 key fsession:{session_id}:history redis_client.lpush(key, f{role}: {content}) redis_client.ltrim(key, 0, 2) # 只留3条 # 在FastAPI路由里使用 app.post(/v1/support) def handle_support_request(request: SupportRequest): # 读取历史 history get_session_history(request.session_id) # 注入到LLM调用 result support_qa( customer_questionrequest.customer_question, product_contexthistory # 这里 ) # 保存本次交互 save_to_session_history(request.session_id, user, request.customer_question) save_to_session_history(request.session_id, assistant, result.answer)这个方案的好处是轻量、解耦。Redis只存文本不存复杂对象ltrim保证内存可控。我在线上用它支撑了2000并发会话Redis内存占用稳定在120MB以内。5.2 工具调用Function Calling实战让Bot能查数据库Declarai原生支持OpenAI的Function Calling。假设你有一个产品数据库想让Bot能实时查价格、库存。第一步定义工具函数from declarai import Declarai import sqlite3 # 假设你有一个products.db def search_product_by_name(name: str) - list: Search products by name in database. Returns list of {id, name, price, stock}. conn sqlite3.connect(products.db) cursor conn.cursor() cursor.execute(SELECT id