CrewAI+FastAPI实现多Agent协作项目

目录:

一、项目介绍

本项目实现一个技术研究员智能体包含两个Agent,分别负责研究最新技术趋势并进行详细分析和撰写报告,并最终调用外部工具把生成的报告以PDF文件保存到本地

1、定义了两个Agent

researcher:

  • role: > 高级技术研究员

  • goal: > 研究 {topic} 的最新技术趋势并进行详细分析。

  • backstory: > 你是该领域的技术专家,擅长探索技术前沿动态,深入挖掘最新技术发展趋势,并能够以简洁明了的方式将其呈现出来。

reporting_writer:

  • role: > 技术趋势报告撰写者

  • goal: > 根据研究员的分析撰写一份关于 {topic} 技术趋势的全面报告,确保易于理解且具有深度。

  • backstory: > 你是一个擅长撰写技术文章的作家,能够将复杂的技术概念用简单的语言解释清楚,同时确保报告具有深度和可读性。

2、定义了两个Task

research_task:

  • description: > 研究 {topic} 的最新技术趋势。请关注最新的技术进展,评估它们的优缺点,并提供详细的分析。

分析内容应该包括至少三段内容,介绍当前趋势、技术的利弊及其潜在影响。

  • expected_output: > 一份包含5个要点的清单,介绍 {topic} 的技术前沿动态,最新技术发展趋势。

  • agent: researcher

reporting_task:

  • description: > 根据研究员提供的技术分析内容,撰写一份关于 {topic}

    的技术趋势报告。请确保报告条理清晰,包含洞察力,并阐述技术的潜在影响。

  • expected_output: > 一份详细的四段报告,解释 {topic} 技术趋势及其影响,适合行业读者阅读。

  • agent: reporting_writer

二、前期准备工作

2.1 CrewAI介绍

1、简介

  • CrewAI是一个用于构建多Agent系统的工具,它能够让多个具有不同角色和目标的Agent共同协作,完成复杂的Task。

  • 该工具可以将Task分解,分配给不同的Agent,借助它们的特定技能和工具,完成各自的职责,最终实现整体任务目标。

官网:https://www.crewai.com/
GitHub:https://github.com/crewAIInc/crewAI

2、核心概念

Agents:

1、 是一个自主可控单元,通过编程可以实现执行任务、作出决定、与其他Agent协作交流。

2、可类比为团队中的一员,拥有特定的技能和任务 。

属性:

  • role(角色):定义Agent在团队中的角色功能

  • goal(目标):Agent实现的目标

  • backstory(背景信息):为Agent提供上下文

Tasks:

分配给Agent的具体任务,提供执行任务所需的所有细节

属性:

  • description(任务描述):简明扼要说明任务要求

  • agent(分配的Agent):分配负责该任务的Agent

  • expected_output(期望输出):任务完成情况的详细描述

  • Tools(工具列表):为Agent提供可用于执行该任务的工具列表

  • output_json(输出json):输出一个json对象,只能输出一种数据格式

  • output_file(工具列表):将任务结果输出到一个文件中,指定输出的文件格式

  • context(上下文):指定其输出被用作该任务上下文的任务

Processes

CrewAI中负责协调Agent执行任务,类似于团队中的项目经理,确保任务分配和执行效率与预定计划保持一致。

目前拥有两种实施机制:

  • sequential(顺序流程):反映了crew中动态的工作流程,以深思熟虑的和系统化的方式推进各项任务,按照任务列表中预定义的顺序执行,一个任务的输出作为下一个任务的上下文

  • hierarchical(分层流程):允许指定一个自定义的管理Agent,负责监督任务执行,包括计划、授权和验证。任务不是预先分配的,而是根据Agent的能力进行任务分配,审查产出并评估任务完成情况

Crews:

1个crew代表一组合作完成一系列任务的Agent

每个crew定义了任务执行策略、Agent协作和整体工作流程

属性:

  • Tasks(任务列表):分配给crew的任务列表
  • Agents(Agent列表):分配给crew的Agent列表
  • Process(背景信息):crew遵循的流程
  • manager_llm(大模型):在hierarchical模式下指定大模型
  • language(语言):指定crew使用的语言
  • language_file(语言文件):指定crew使用的语言文件

Pipleline:

在CrewAI中,pipleline代表一种结构化的工作流程,允许多个crew顺序或并行执行

提供了一种组织涉及多个阶段的复杂流程的方法,其中一个阶段的输出可作为后续阶段的输入。

关键术语:

  • Stage:pipleline中的1个独立部分,可以是1个顺序crews,也可以是一个并行的crews
  • Run:运行pipleling处理的单个实例
  • Branch:Stage内的并行执行
  • Trace:单个输入在整个pipleline中的运行轨迹、捕捉它所经历的路径和转换

2.2 anaconda、pycharm 安装

  • anaconda:提供python虚拟环境,官网下载对应系统版本的安装包安装即可
  • pycharm:提供集成开发环境,官网下载社区版本安装包安装即可

2.3 GPT大模型使用方案

可以使用代理的方式,具体代理方案自己选择。

2.4 非GPT大模型(国产大模型)使用方案,OneAPI安装、部署、创建渠道和令牌

1、OneAPI是什么

官方介绍:是OpenAI接口的管理、分发系统

支持 Azure、Anthropic Claude、Google PaLM 2 & Gemini、智谱 ChatGLM、百度文心一言、讯飞星火认知、阿里通义千问、360 智脑以及腾讯混元

2、安装、部署、创建渠道和令牌

创建渠道:大模型类型(通义千问)、APIKey(通义千问申请的真实有效的APIKey)

创建令牌:创建OneAPI的APIKey,后续代码中直接调用此APIKey。

2.5 本地开源大模型使用方案,Ollama

1、Ollama是什么

Ollama是一个轻量级、跨平台的工具和库,专门为本地大语言模型(LLM)的部署和运行提供支持

它旨在简化在本地环境中运行大模型的过程,不需要依赖云服务或外部API,使用户能够更好地掌控和使用大型模型

2、Ollama安装、启动、下载大模型

安装Ollama,进入官网https://ollama.com下载对应系统版本直接安装即可

启动Ollama,安装所需要使用的本地模型,执行指令进行安装即可,参考如下:

powershell 复制代码
ollama pull qwen2:latest                                                
ollama pull llama3.1:latest                                             
ollama pull gemma2:latest                                                  
ollama pull nomic-embed-text:latest     

其中:

  1. qwen2:latest(7b),对应版本有0.5b、1.5b、7b、72b;

  2. llama3.1:latest(8b),对应版本有8b、70b、405b;

  3. gemma2:latest(9b),对应版本有2b、9b、27b等

  4. embedding模型:nomic-embed-text:latest(也就是1.5版本)

三、项目初始化

3.1 下载源码

GitHub或Gitee中下载工程文件到本地,下载地址如下:

https://github.com/NanGePlus/CrewAITest

https://gitee.com/NanGePlus/CrewAITest

3.2 构建项目

使用pycharm构建一个项目,为项目配置虚拟python环境

项目名称:CrewAITest

3.3 将相关代码拷贝到项目工程中

直接将下载的文件夹中的文件拷贝到新建的项目目录中

3.4 安装项目依赖

命令行终端中执行cd crewAIWithResearcher 命令进入到该文件夹内,然后执行如下命令安装依赖包

powershell 复制代码
pip install -r requirements.txt    

每个软件包后面都指定了本次视频测试中固定的版本号

四、项目测试

1、运行main脚本启动API服务

在使用python main.py命令启动脚本前,需根据自己的实际情况调整代码中的如下参数:
openai模型相关配置 根据自己的实际情况进行调整

powershell 复制代码
OPENAI_API_BASE = "https://api.wlai.vip/v1"            
OPENAI_CHAT_API_KEY = "sk-XmrIEFplNArLlYa0E8C5A7C5F82041FdBd923e9d115746D0"          
OPENAI_CHAT_MODEL = "gpt-4o-mini"     

非gpt大模型相关配置(oneapi方案 通义千问为例) 根据自己的实际情况进行调整

powershell 复制代码
ONEAPI_API_BASE = "http://139.224.72.218:3000/v1"            
ONEAPI_CHAT_API_KEY = "sk-0FxX9ncd0yXjTQF877Cc9dB6B2F44aD08d62805715821b85"               
ONEAPI_CHAT_MODEL = "qwen-max"      

本地大模型相关配置(Ollama方案 llama3.1:latest为例) 根据自己的实际情况进行调整

powershell 复制代码
OLLAMA_API_BASE = "http://localhost:11434/v1"                
OLLAMA_CHAT_API_KEY = "ollama"          
OLLAMA_CHAT_MODEL = "llama3.1:latest"        

openai:调用gpt大模型;oneapi:调用非gpt大模型;ollama:调用本地大模型

powershell 复制代码
MODEL_TYPE = "openai"      

API服务设置相关 根据自己的实际情况进行调整

powershell 复制代码
PORT = 8012  # 服务访问的端口         

2、运行apiTest脚本进行测试

在运行python apiTest.py命令启动脚本前,需根据自己的实际情况调整代码中的如下参数:
调整1:默认非流式输出 True or False

powershell 复制代码
stream_flag = False      

调整2:检查URL地址中的IP和PORT是否和main脚本中相同

powershell 复制代码
url = "http://localhost:8012/v1/chat/completions"           

五、代码分析

1、项目结构

2、mian.py程序入口类

python 复制代码
# 导入依赖包
import os
import sys
import re
import uuid
import time
import json
import asyncio
from contextlib import asynccontextmanager
from pydantic import BaseModel, Field
from typing import List, Optional, Dict
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
import uvicorn
from langchain_openai import ChatOpenAI
from crew import CrewtestprojectCrew  #crew.py中引入封装的方法CrewtestprojectCrew



# 模型全局参数配置  根据自己的实际情况进行调整
# openai模型相关配置 根据自己的实际情况进行调整
OPENAI_API_BASE = "https://api.wlai.vip/v1"
OPENAI_CHAT_API_KEY = "sk-CU5Dncdg7OebzZm4Fa532b1cBf134447A93fE109Bd2d1b19"
OPENAI_CHAT_MODEL = "gpt-4o-mini"
# 非gpt大模型相关配置(oneapi方案 通义千问为例) 根据自己的实际情况进行调整
ONEAPI_API_BASE = "http://139.224.72.218:3000/v1"
ONEAPI_CHAT_API_KEY = "sk-0FxX9ncd0yXjTQF877Cc9dB6B2F44aD08d62805715821b85"
ONEAPI_CHAT_MODEL = "qwen-max"
# 本地大模型相关配置(Ollama方案 llama3.1:latest为例) 根据自己的实际情况进行调整
OLLAMA_API_BASE = "http://localhost:11434/v1"
OLLAMA_CHAT_API_KEY = "ollama"
OLLAMA_CHAT_MODEL = "llama3.1:latest"


# 初始化LLM模型
model = None
# API服务设置相关  根据自己的实际情况进行调整
PORT = 8012  # 服务访问的端口
# openai:调用gpt大模型;oneapi:调用非gpt大模型;ollama:调用本地大模型
MODEL_TYPE = "openai"



# 定义Message类
class Message(BaseModel):
    role: str
    content: str
# 定义ChatCompletionRequest类
class ChatCompletionRequest(BaseModel):
    messages: List[Message]
    stream: Optional[bool] = False
# 定义ChatCompletionResponseChoice类
class ChatCompletionResponseChoice(BaseModel):
    index: int
    message: Message
    finish_reason: Optional[str] = None
# 定义ChatCompletionResponse类
class ChatCompletionResponse(BaseModel):
    id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
    object: str = "chat.completion"
    created: int = Field(default_factory=lambda: int(time.time()))
    choices: List[ChatCompletionResponseChoice]
    system_fingerprint: Optional[str] = None


# 定义了一个异步函数lifespan,它接收一个FastAPI应用实例app作为参数。这个函数将管理应用的生命周期,包括启动和关闭时的操作
# 函数在应用启动时执行一些初始化操作
# 函数在应用关闭时执行一些清理操作
# @asynccontextmanager 装饰器用于创建一个异步上下文管理器,它允许在yield之前和之后执行特定的代码块,分别表示启动和关闭时的操作
@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动时执行
    # 申明引用全局变量,在函数中被初始化,并在整个应用中使用
    global MODEL_TYPE, model
    global ONEAPI_API_BASE, ONEAPI_CHAT_API_KEY, ONEAPI_CHAT_MODEL
    global OPENAI_API_BASE, OPENAI_CHAT_API_KEY, OPENAI_CHAT_MODEL
    global OLLAMA_API_BASE, OLLAMA_CHAT_API_KEY, OLLAMA_CHAT_MODEL
    # 根据自己实际情况选择调用model和embedding模型类型
    try:
        print("正在初始化模型")
        # 根据MODEL_TYPE选择初始化对应的模型,默认使用gpt大模型
        if MODEL_TYPE == "oneapi":
            # 实例化一个oneapi客户端对象
            model = ChatOpenAI(
                base_url=ONEAPI_API_BASE,
                api_key=ONEAPI_CHAT_API_KEY,
                model=ONEAPI_CHAT_MODEL,  # 本次使用的模型
                # temperature=0,# 发散的程度,一般为0
                # timeout=None,# 服务请求超时
                # max_retries=2,# 失败重试最大次数
            )
        elif MODEL_TYPE == "ollama":
            # 实例化一个ChatOpenAI客户端对象
            model = ChatOpenAI(
                base_url=OLLAMA_API_BASE,# 请求的API服务地址
                api_key=OLLAMA_CHAT_API_KEY,# API Key
                model=OLLAMA_CHAT_MODEL,# 本次使用的模型
                # temperature=0,# 发散的程度,一般为0
                # timeout=None,# 服务请求超时
                # max_retries=2,# 失败重试最大次数
            )
        else:
            # 实例化一个ChatOpenAI客户端对象
            model = ChatOpenAI(
                base_url=OPENAI_API_BASE,# 请求的API服务地址
                api_key=OPENAI_CHAT_API_KEY,# API Key
                model=OPENAI_CHAT_MODEL,# 本次使用的模型
                # temperature=0,# 发散的程度,一般为0
                # timeout=None,# 服务请求超时
                # max_retries=2,# 失败重试最大次数
            )

        print("LLM初始化完成")

    except Exception as e:
        print(f"初始化过程中出错: {str(e)}")
        # raise 关键字重新抛出异常,以确保程序不会在错误状态下继续运行
        raise

    # yield 关键字将控制权交还给FastAPI框架,使应用开始运行
    # 分隔了启动和关闭的逻辑。在yield 之前的代码在应用启动时运行,yield 之后的代码在应用关闭时运行
    yield
    # 关闭时执行
    print("正在关闭...")


# lifespan 参数用于在应用程序生命周期的开始和结束时执行一些初始化或清理工作
app = FastAPI(lifespan=lifespan)


# POST请求接口,与大模型进行知识问答
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
    if not model:
        print("服务未初始化")
        raise HTTPException(status_code=500, detail="服务未初始化")
    try:
        # print(f"收到聊天完成请求: {request}")
        query_prompt = request.messages[-1].content
        print(f"用户问题是: {query_prompt}")
        # 执行crew
        inputs = {
            "topic": query_prompt
        }
        # 传入model,指定crew中的Agent使用什么大模型
        result = CrewtestprojectCrew(model).crew().kickoff(inputs=inputs)
        # 将返回的数据转成string类型
        formatted_response = str(result)
        print(f"LLM最终回复结果: {formatted_response}")

        # 处理流式响应
        if request.stream:
            # 定义一个异步生成器函数,用于生成流式数据
            async def generate_stream():
                # 为每个流式数据片段生成一个唯一的chunk_id
                chunk_id = f"chatcmpl-{uuid.uuid4().hex}"
                # 将格式化后的响应按行分割
                lines = formatted_response.split('\n')
                # 历每一行,并构建响应片段
                for i, line in enumerate(lines):
                    # 创建一个字典,表示流式数据的一个片段
                    chunk = {
                        "id": chunk_id,
                        "object": "chat.completion.chunk",
                        "created": int(time.time()),
                        # "model": request.model,
                        "choices": [
                            {
                                "index": 0,
                                "delta": {"content": line + '\n'}, # if i > 0 else {"role": "assistant", "content": ""},
                                "finish_reason": None
                            }
                        ]
                    }
                    # 将片段转换为JSON格式并生成
                    yield f"{json.dumps(chunk)}\n"
                    # 每次生成数据后,异步等待0.5秒
                    await asyncio.sleep(0.5)
                # 生成最后一个片段,表示流式响应的结束
                final_chunk = {
                    "id": chunk_id,
                    "object": "chat.completion.chunk",
                    "created": int(time.time()),
                    "choices": [
                        {
                            "index": 0,
                            "delta": {},
                            "finish_reason": "stop"
                        }
                    ]
                }
                yield f"{json.dumps(final_chunk)}\n"

            # 返回fastapi.responses中StreamingResponse对象,流式传输数据
            # media_type设置为text/event-stream以符合SSE(Server-SentEvents) 格式
            return StreamingResponse(generate_stream(), media_type="text/event-stream")
        # 处理非流式响应处理
        else:
            response = ChatCompletionResponse(
                choices=[
                    ChatCompletionResponseChoice(
                        index=0,
                        message=Message(role="assistant", content=formatted_response),
                        finish_reason="stop"
                    )
                ]
            )
            # print(f"发送响应内容: \n{response}")
            # 返回fastapi.responses中JSONResponse对象
            # model_dump()方法通常用于将Pydantic模型实例的内容转换为一个标准的Python字典,以便进行序列化
            return JSONResponse(content=response.model_dump())

    except Exception as e:
        print(f"处理聊天完成时出错:\n\n {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))



if __name__ == "__main__":
    print(f"在端口 {PORT} 上启动服务器")
    # uvicorn是一个用于运行ASGI应用的轻量级、超快速的ASGI服务器实现
    # 用于部署基于FastAPI框架的异步PythonWeb应用程序
    uvicorn.run(app, host="0.0.0.0", port=PORT)

3、crew.py

python 复制代码
# 核心功能:在CrewAI中定义Agent和Task,并通过Crew来管理这些Agent和Task的执行流程

# 导入相关的依赖包
from crewai import Agent, Crew, Process, Task
# CrewBase是一个装饰器,标记一个类为CrewAI项目。agent、task和crew装饰器用于定义agent、task和crew
from crewai.project import CrewBase, agent, crew, task
# 使用自定义工具
from tools.custom_tool import saveText2Pdf #调用custom_tool.py将结果输入到pdf文件中



# 定义了一个CrewtestprojectCrew类并应用了@CrewBase装饰器初始化项目
# 这个类代表一个完整的CrewAI项目
@CrewBase
class CrewtestprojectCrew():
	# agents_config和tasks_config分别指向agent和task的配置文件,存放在config目录下
	agents_config = 'config/agents.yaml'
	tasks_config = 'config/tasks.yaml'

	def __init__(self, model):
		# Agent使用的大模型
		self.model = model

	# 通过@agent装饰器定义一个函数researcher,返回一个Agent实例
	# 该代理读取agents_config中的researcher配置
	# 参数verbose=True用于输出调试信息
	# tools=[MyCustomTool()] 表示代理可以加载自定义工具,但此处为注释,需根据需求自行加载。
	@agent
	def researcher(self) -> Agent:
		return Agent(
			config=self.agents_config['researcher'],
			verbose=True,
			llm=self.model
		)

	@agent
	def reporting_writer(self) -> Agent:
		return Agent(
			config=self.agents_config['reporting_writer'],
			verbose=True,
			llm=self.model,
			# tools=[pdfSaveTool]
		)

	# 通过@task装饰器定义research_task,返回一个Task实例
	# 配置文件为tasks.yaml中的research_task部分
	@task
	def research_task(self) -> Task:
		return Task(
			config=self.tasks_config['research_task'],
		)

	@task
	def reporting_task(self) -> Task:
		return Task(
			config=self.tasks_config['reporting_task'],
			# 使用工具
			tools=[saveText2Pdf]
		)

	# Crew类将agent和task组合成一个执行队列,并根据指定的执行流程进行任务调度
	# 通过@crew装饰器定义crew,创建一个Crew实例
	# agents=self.agents和tasks=self.tasks分别自动获取@agent和@task装饰器生成的agent和task
	# process=Process.sequential指定agent执行顺序为顺序执行模式
	# process=Process.hierarchical指定agent执行顺序为层次化执行
	@crew
	def crew(self) -> Crew:
		return Crew(
			agents=self.agents,
			tasks=self.tasks,
			process=Process.sequential,
			verbose=True
		)

4、apiTest.py测试类

python 复制代码
import requests
import json
import logging


# 设置日志模版
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


url = "http://localhost:8012/v1/chat/completions"
headers = {"Content-Type": "application/json"}


# 构造消息体
# 默认非流式输出 True or False
stream_flag = False
# 用户输入
content = "人工智能"
data = {
    "messages": [{"role": "user", "content": content}],
    "stream": stream_flag,
}


# 接收流式输出
if stream_flag:
    try:
        with requests.post(url, stream=True, headers=headers, data=json.dumps(data)) as response:
            for line in response.iter_lines():
                if line:
                    json_str = line.decode('utf-8').strip("data: ")
                    # 检查是否为空或不合法的字符串
                    if not json_str:
                        logger.info(f"收到空字符串,跳过...")
                        continue
                    # 确保字符串是有效的JSON格式
                    if json_str.startswith('{') and json_str.endswith('}'):
                        try:
                            data = json.loads(json_str)
                            if data['choices'][0]['finish_reason'] == "stop":
                                logger.info(f"接收JSON数据结束")
                            else:
                                logger.info(f"流式输出,响应内容是: {data['choices'][0]['delta']['content']}")
                        except json.JSONDecodeError as e:
                            logger.info(f"JSON解析错误: {e}")
                    else:
                        print(f"无效JSON格式: {json_str}")
    except Exception as e:
        print(f"Error occurred: {e}")

# 接收非流式输出处理
else:
    # 发送post请求
    response = requests.post(url, headers=headers, data=json.dumps(data))
    # logger.info(f"接收到返回的响应原始内容: {response.json()}\n")
    content = response.json()['choices'][0]['message']['content']
    logger.info(f"非流式输出,响应内容是: {content}\n")

总结:

整体来说项目比较简单,总体逻辑都在main.py(这个是程序入口文件,并使用fastapi来发布接口服务)和crew.py(封装了crew的方法,绑定任务和agent);这两个类就完成了主体逻辑,apiTest.py(去测试模型的处理效果的)。

在apiTest.py目前是写死的用户输入:"人工智能",然后测试ollma本地大模型响应的一个回答。

六、全自动化问答

那就是要做前端页面,然后用户选择对应模型以及输入内容后,发送请求到后端服务处理,以及启动服务就默认进入聊天界面。

1. 前端界面实现

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>大模型对话界面</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        #chat-container {
            border: 1px solid #ccc;
            border-radius: 5px;
            height: 500px;
            overflow-y: auto;
            margin-bottom: 10px;
            padding: 10px;
        }
        #input-container {
            display: flex;
            gap: 10px;
        }
        #user-input {
            flex-grow: 1;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }
        button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        button:hover {
            background-color: #45a049;
        }
        .message {
            margin-bottom: 10px;
            padding: 8px 12px;
            border-radius: 5px;
        }
        .user-message {
            background-color: #e3f2fd;
            margin-left: 20%;
        }
        .assistant-message {
            background-color: #f1f1f1;
            margin-right: 20%;
        }
        #model-selector {
            margin-bottom: 10px;
            padding: 8px;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <h1>大模型对话界面</h1>
    
    <select id="model-selector">
        <option value="openai">GPT模型</option>
        <option value="oneapi">通义千问</option>
        <option value="ollama">本地Llama3</option>
    </select>
    
    <div id="chat-container"></div>
    
    <div id="input-container">
        <input type="text" id="user-input" placeholder="输入您的问题..." autocomplete="off">
        <button id="send-button">发送</button>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const chatContainer = document.getElementById('chat-container');
            const userInput = document.getElementById('user-input');
            const sendButton = document.getElementById('send-button');
            const modelSelector = document.getElementById('model-selector');
            
            // 添加消息到聊天界面
            function addMessage(role, content) {
                const messageDiv = document.createElement('div');
                messageDiv.classList.add('message');
                messageDiv.classList.add(role + '-message');
                messageDiv.textContent = content;
                chatContainer.appendChild(messageDiv);
                chatContainer.scrollTop = chatContainer.scrollHeight;
            }
            
            // 发送消息到后端
            async function sendMessage() {
                const message = userInput.value.trim();
                if (!message) return;
                
                const selectedModel = modelSelector.value;
                
                // 显示用户消息
                addMessage('user', message);
                userInput.value = '';
                
                // 显示"思考中..."提示
                const thinkingDiv = document.createElement('div');
                thinkingDiv.classList.add('message');
                thinkingDiv.classList.add('assistant-message');
                thinkingDiv.textContent = '思考中...';
                thinkingDiv.id = 'thinking-message';
                chatContainer.appendChild(thinkingDiv);
                chatContainer.scrollTop = chatContainer.scrollHeight;
                
                try {
                    // 调用后端API
                    const response = await fetch(`http://localhost:${PORT}/v1/chat/completions`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-Model-Type': selectedModel
                        },
                        body: JSON.stringify({
                            messages: [{role: 'user', content: message}],
                            stream: false
                        })
                    });
                    
                    if (!response.ok) {
                        throw new Error(`请求失败: ${response.status}`);
                    }
                    
                    const data = await response.json();
                    const reply = data.choices[0].message.content;
                    
                    // 移除"思考中..."提示,显示实际回复
                    document.getElementById('thinking-message').remove();
                    addMessage('assistant', reply);
                } catch (error) {
                    console.error('请求出错:', error);
                    document.getElementById('thinking-message').remove();
                    addMessage('assistant', `出错: ${error.message}`);
                }
            }
            
            // 点击发送按钮或按Enter键发送消息
            sendButton.addEventListener('click', sendMessage);
            userInput.addEventListener('keypress', function(e) {
                if (e.key === 'Enter') {
                    sendMessage();
                }
            });
        });
    </script>
</body>
</html>

2. 修改后端服务支持模型切换

python 复制代码
# 在FastAPI应用中添加这个中间件
@app.middleware("http")
async def add_model_type_header(request: Request, call_next):
    # 从请求头中获取模型类型
    model_type = request.headers.get('x-model-type', MODEL_TYPE)
    
    # 将模型类型存储在请求状态中
    request.state.model_type = model_type
    
    response = await call_next(request)
    return response

# 修改chat_completions端点,使用请求中的模型类型
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest, http_request: Request):
    global model
    
    # 从请求状态中获取模型类型
    model_type = http_request.state.model_type
    
    # 根据模型类型选择模型配置
    if model_type == "oneapi":
        model = ChatOpenAI(
            base_url=ONEAPI_API_BASE,
            api_key=ONEAPI_CHAT_API_KEY,
            model=ONEAPI_CHAT_MODEL,
        )
    elif model_type == "ollama":
        model = ChatOpenAI(
            base_url=OLLAMA_API_BASE,
            api_key=OLLAMA_CHAT_API_KEY,
            model=OLLAMA_CHAT_MODEL,
        )
    else:
        model = ChatOpenAI(
            base_url=OPENAI_API_BASE,
            api_key=OPENAI_CHAT_API_KEY,
            model=OPENAI_CHAT_MODEL,
        )

    # 其余代码保持不变...
    if not model:
        print("服务未初始化")
        raise HTTPException(status_code=500, detail="服务未初始化")
    
    try:
        # 原有处理逻辑...

3、使用FastAPI模板服务默认展示聊天界面(推荐)

3.1、创建目录结构

python 复制代码
your_project/
├── main.py
├── static/
│   └── index.html
├── templates/
│   └── index.html

3.2、修改main.py

python 复制代码
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import webbrowser
import threading
import time

app = FastAPI(lifespan=lifespan)

# 挂载静态文件目录
app.mount("/static", StaticFiles(directory="static"), name="static")

# 设置模板目录
templates = Jinja2Templates(directory="templates")

@app.get("/")
async def serve_html(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

def open_browser():
    time.sleep(1)
    webbrowser.open(f"http://localhost:{PORT}")

if __name__ == "__main__":
    threading.Thread(target=open_browser).start()
    print(f"在端口 {PORT} 上启动服务器")
    uvicorn.run(app, host="0.0.0.0", port=PORT)

3.3、问题总结

1、为啥static和template下都有index.html?分别作用是啥?

一、static/index.html:静态文件(无需动态处理)

作用:

存储纯静态页面,内容固定不变,直接由浏览器加载,不经过后端模板引擎处理。

典型使用场景:

  • 独立的静态网站(如纯 HTML/CSS/JS 构建的单页应用)。

  • 不需要后端动态数据注入的页面(如帮助文档、关于我们)。

  • 前端资源文件(如图片、CSS、JS)通常也放在 static/ 目录下,供静态页面引用。

访问方式:

python 复制代码
通过 URL 直接访问静态文件路径,例如:
http://localhost:8012/static/index.html

配置代码(FastAPI):

python 复制代码
from fastapi.staticfiles import StaticFiles

app.mount("/static", StaticFiles(directory="static"), name="static")

二、templates/index.html:模板文件(需动态处理)

作用:

存储动态模板页面,需要通过后端模板引擎(如 Jinja2)渲染,可注入动态数据(如用户信息、数据库查询结果)。

典型使用场景:

  • 需要后端动态生成内容的页面(如用户个人中心、动态列表)。

  • 包含模板语法的页面(如条件判断 {% if %}、循环 {% for %}、变量 {{ variable }})。

  • 与后端路由绑定的页面,通过后端接口返回渲染后的 HTML。

访问方式:

通过后端路由间接访问,例如:

python 复制代码
from fastapi.templating import Jinja2Templates
from fastapi import Request

templates = Jinja2Templates(directory="templates")

@app.get("/")  # 路由映射
async def read_root(request: Request):
    # 注入动态数据到模板
    return templates.TemplateResponse(
        "index.html",  # 模板文件名
        {"request": request, "username": "张三"}  # 动态变量
    )

此时访问 http://localhost:8012/,后端会渲染 templates/index.html 并返回结果。

项目地址:

https://github.com/NanGePlus/CrewAITest/tree/main/crewAIWithResearcher

crewai官网地址:

https://docs.crewai.com/

相关推荐
鹏北海7 小时前
从弹窗变胖到 npm 依赖管理:一次完整的问题排查记录
前端·npm·node.js
布列瑟农的星空7 小时前
js中的using声明
前端
薛定谔的猫27 小时前
Cursor 系列(2):使用心得
前端·ai编程·cursor
全栈独立开发者7 小时前
点餐系统装上了“DeepSeek大脑”:基于 Spring AI + PgVector 的 RAG 落地指南
java·人工智能·spring
用户904706683577 小时前
后端问前端:我的接口请求花了多少秒?为啥那么慢,是你慢还是我慢?
前端
深念Y7 小时前
仿B站项目 前端 4 首页 顶层导航栏
前端·vue·ai编程·导航栏·bilibili·ai开发
dmonstererer7 小时前
【k8s设置污点/容忍】
java·容器·kubernetes
dragonZhang7 小时前
基于 Agent Skills 的 UI 重构实践:从 Demo 到主题化界面的升级之路
前端·ai编程·claude
super_lzb7 小时前
mybatis拦截器ParameterHandler详解
java·数据库·spring boot·spring·mybatis
程序之巅7 小时前
VS code 远程python代码debug
android·java·python