这篇文章起源于小毕超的《无需 GPU 服务器,借助 OpenRouter 零成本搭建自己的大模型助手-CSDN博客》。关于OpenRouter的介绍,此文章已经比较全面。该文章中使用 Python + tornado
实现 Web
服务,前端使用基础的 Html + Jquery编写的
server.py、app.py和html文件为我提供了重要帮助。之所以写这篇文章,是因为在使用server.py中出现了“405 服务接口调用异常”。在利用chrome devtool跟踪网络请求后,发现tornado.web.Application的“/assistant”路由,在向app.py发起post请求之后,还将发起第二次options请求,而文章中的Assistant并没有加入响应options请求的处理代码。
经询问kimi AIKimi.ai - 帮你看更大的世界,了解问题出在“在使用Tornado搭建的服务端时,如果遇到发起OPTIONS请求出错的问题,这通常是由于预请求(pre-flight request)导致的。在HTTP协议中,OPTIONS请求是一种预请求,用于客户端在发送实际请求之前,询问服务器是否允许某个HTTP方法(如POST、GET、PUT、DELETE等)”。解决方案为:
-
配置CORS:可以使用Tornado的
tornado.web.CorsFilter
来设置CORS策略,或者手动设置响应头Access-Control-Allow-Methods
。 -
添加OPTIONS请求处理器:在Tornado中,可以为路由添加一个OPTIONS方法的处理器,返回允许的HTTP方法。
server.py中对cors的设置已经由set_default_headers正确设置,但Assistant类中缺少options处理方法。修改后的server.py代码如下:
from tornado.concurrent import run_on_executor
from tornado.web import RequestHandler
import tornado.gen
from openai import OpenAI
import json
class Assistant(RequestHandler):
model = "google/gemma-7b-it:free"
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key="sk-##########################",
)
default_prompt = "You are an AI assistant that helps people find information."
def prepare(self):
self.executor = self.application.pool
def set_default_headers(self):
self.set_header('Access-Control-Allow-Origin', "*")
self.set_header('Access-Control-Allow-Headers', "Origin, X-Requested-With, Content-Type, Accept")
self.set_header('Access-Control-Allow-Methods', "GET, POST, PUT, DELETE, OPTIONS")
def options(self):
# 直接调用set_default_headers来设置CORS头部
self.set_default_headers()
# OPTIONS请求不需要响应体,因此直接finish
self.finish()
@tornado.gen.coroutine
def post(self):
json_data = json.loads(self.request.body)
if 'questions' not in json_data or 'history' not in json_data:
self.write({
"code": 400,
"message": "缺少必填参数"
})
return
questions = json_data['questions']
history = json_data['history']
result = yield self.do_handler(questions, history)
self.write(result)
@run_on_executor
def do_handler(self, questions, history):
try:
answer, history = self.llm(questions, history)
return {
"code": 200,
"message": "success",
"answer": answer,
"history": history
}
except Exception as e:
return {
"code": 400,
"message": str(e)
}
def llm(self, user_prompt, messages, system_prompt=default_prompt):
if not messages:
messages = []
messages.append({"role": "user", "content": user_prompt})
completion = self.client.chat.completions.create(
extra_headers={
"HTTP-Referer": "http://localhost:8088",
"X-Title": "test",
},
model=self.model,
messages=messages,
max_tokens=2048
)
answer = completion.choices[0].message.content
messages.append({"role": "assistant", "content": answer})
return answer, messages
app.py和本地html文件无需更改。
最后,openrouter中还有其他免费的LLM,但是免费的LLM都有一定的限制,google/gemma-7b-it:free在处理中文方面还存在不足,会出现中英文混排输出和乱码的问题。