介绍
目前项目的架构为前端为VUE框架,后端应用为langchain框架,服务部署对外支持为FastAPI框架,后端服务运行在Uvicorn。
FastAPI
FastAPI是一个用于构建API的现代、快速(高性能)的web框架,使用Python 3.8+并基于标准的Python类型提示。
关键特性:
- 快速:可与 NodeJS 和 Go 并肩的极高性能(归功于
Starlette 和 Pydantic
)。最快的 Python web 框架之一。 - 高效编码:提高功能开发速度约 200% 至 300%。
- 更少 bug:减少约 40% 的人为(开发者)导致错误。
- 智能:极佳的编辑器支持。处处皆可自动补全,减少调试时间。
- 简单:设计的易于使用和学习,阅读文档的时间更短。
- 简短:使代码重复最小化。通过不同的参数声明实现丰富功能。bug 更少。
- 健壮:生产可用级别的代码。还有自动生成的交互式文档。
- 标准化:基于(并完全兼容)API 的相关开放标准:OpenAPI (以前被称为
Swagger
) 和 JSON Schema。 - 官方文档:https://fastapi.tiangolo.com/zh/
Uvicorn
Uvicorn是一个基于ASGI(Asynchronous Server Gateway Interface)的异步web服务器,用于运行异步Python web应用程序。它是由编写FastAPI框架的开发者设计的,旨在提供高性能和低延迟的Web服务。
简单实践:带有历史记录的chat服务
快速启动
在APP文件夹下构建app.py文件
- 第一步: 导入FastAPI;
- 第二步: 创建 FastAPI实例app;
- 第三步: 使用@app.get注册路由,除了@app.get之外还支持:@app.post、@app.put、@app.delete..等方法;
- 第四步: 使用uvicorn启动服务
from fastapi import FastAPI
from fastapi import Request
import uvicorn
import os
app = FastAPI()
@app.get("/hhhh")
async def test(request: Request):
params = request.query_params
print(params.get("prompt"))
return {"msg": params.get("prompt")}
uvicorn.run("app:app", host="localhost", port=8088)
跨域
原因
假设你的浏览器中有一个前端运行在 http://localhost:8080,并且它的 JavaScript 正在尝试与运行在 http://localhost 的后端通信(因为我们没有指定端口,浏览器会采用默认的端口 80)。
然后,浏览器会向后端发送一个 HTTP OPTIONS 请求,如果后端发送适当的 headers 来授权来自这个不同源(http://localhost:8080)的通信,浏览器将允许前端的 JavaScript 向后端发送请求。
为此,后端必须有一个「允许的源」列表。
在这种情况下,它必须包含 http://localhost:8080,前端才能正常工作。
因此,为了一切都能正常工作,最好显式地指定允许的源。
使用CORSMiddleware
from fastapi.middleware.cors import CORSMiddleware # 解决跨域问题
app.add_middleware(
CORSMiddleware,
# 允许跨域的源列表,例如 ["http://www.example.org"] 等等,["*"] 表示允许任何源
allow_origins=["*"],
# 跨域请求是否支持 cookie,默认是 False,如果为 True,allow_origins 必须为具体的源,不可以是 ["*"]
allow_credentials=False,
# 允许跨域请求的 HTTP 方法列表,默认是 ["GET"]
allow_methods=["*"],
# 允许跨域请求的 HTTP 请求头列表,默认是 [],可以使用 ["*"] 表示允许所有的请求头
# 当然 Accept、Accept-Language、Content-Language 以及 Content-Type 总之被允许的
allow_headers=["*"],
# 可以被浏览器访问的响应头, 默认是 [],一般很少指定
# expose_headers=["*"]
# 设定浏览器缓存 CORS 响应的最长时间,单位是秒。默认为 600,一般也很少指定
# max_age=1000
)
目录结构
通常我们开发一个Python服务,都不会将所有代码放到一个文件里,就像我们不会把衣服、鞋子、袜子、食物这些统统装到一个麻袋里一样; 而是会根据功能或者其他规则,分类存放,FastAPI为我们提供了一个模板,具体如下:
.
├── app # 「app」是一个 Python 包
│ ├── __init__.py # 这个文件使「app」成为一个 Python 包
│ ├── main.py # 「main」模块,例如 import app.main
│ ├── dependencies.py # 「dependencies」模块,例如 import app.dependencies
│ └── routers # 「routers」是一个「Python 子包」
│ │ ├── __init__.py # 使「routers」成为一个「Python 子包」
│ │ ├── items.py # 「items」子模块,例如 import app.routers.items
│ │ └── users.py # 「users」子模块,例如 import app.routers.users
│ └── internal # 「internal」是一个「Python 子包」
│ ├── __init__.py # 使「internal」成为一个「Python 子包」
│ └── admin.py # 「admin」子模块,例如 import app.internal.admin
由于处于初步尝试阶段,我们在APP文件夹下创建app.py,另建brunch文件夹来测试路由,里面有一个用于带有历史记录的chat服务的py文件chat.py。
路由加载
我们在子文件夹下实现了各种业务逻辑,要怎样把请求服务由app.py转发出去呢。
一个简单但不规范的操作
在chat.py下:
from fastapi import APIRouter
chat = APIRouter()
在app.py下:
from brunch.chat import chat
app.include_router(chat, prefix="/chat", tags=["hhh"])
这样就能将以"/chat"为前缀的请求转发到对应的路由下。
业务逻辑
带有历史记录的chat服务重点就是将历史记录加入message传递给LLM,历史消息由前端传递给后端,后端只需要将其组织好传递给大模型。请求返回大模型输出,即问题的回答。
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.schema import StrOutputParser
from langchain_core.messages import HumanMessage
from pydantic import BaseModel
from typing import List
class Msg(BaseModel):
msg: str
class Msgs(BaseModel):
msgs: List[Msg]
@chat.post("/1")
async def chatbot(msgs: Msgs):
chat_history = []
count = 0
while count < len(msgs.msgs) - 1:
human_msg = msgs.msgs[count].msg
count += 1
chat_history.extend([HumanMessage(content=human_msg), msgs.msgs[count].msg])
count += 1
query_text = msgs.msgs[count].msg
print(query_text)
chatLLM = ChatTongyi(
streaming=True,
model_name="qwen-turbo",
)
qa_system_prompt = '''
你是一个问题回答助手,请尽量精简地回答用户的问题。
'''
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", qa_system_prompt),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{question}"),
]
)
chat_with_history_chain = (
qa_prompt
| chatLLM
| StrOutputParser()
)
ans = chat_with_history_chain.invoke({"question": query_text, "chat_history": chat_history})
return {"msg": ans}
效果
由现有前端进行演示:
后端响应:
INFO: Started server process [26524]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://localhost:8088 (Press CTRL+C to quit)
INFO: ::1:52287 - "OPTIONS /chat/1 HTTP/1.1" 200 OK
大模型是什么
INFO: ::1:52287 - "POST /chat/1 HTTP/1.1" 200 OK
“大”是什么意思
INFO: ::1:52288 - "POST /chat/1 HTTP/1.1" 200 OK