为了了解AI中聊天请求的流程,文档中描述了一个调用ollama接口的接口例子,开发语言使用Python,示例中增加了工具的使用。
注意:代码中的有些错误可自行调整,有些参数和类没贴出。
1.流程大致如下

2.工具的定义 tool.py
import jsonimport httpximport osfrom dotenv import load_dotenvload_dotenv()ORDER_SERVICE_URL = os.getenv("ORDER_SERVICE_URL", "http://localhost:8080/api/orders")# 工具定义(符合OpenAI function calling格式)TOOLS = [{"type": "function","function": {"name": "get_order_info","description": "查询指定订单号的详细信息,包括金额、状态、商品等","parameters": {"type": "object","properties": {"order_id": {"type": "string","description": "订单号,例如 ORD123"}},"required": ["order_id"],"additionalProperties": False}}},{"type": "function","function": {"name": "get_weather","description": "查询某个城市的当前天气","parameters": {"type": "object","properties": {"city": {"type": "string","description": "城市名称,例如 北京"}},"required": ["city"],"additionalProperties": False}}}]async def execute_tool(tool_name: str, arguments: dict) -> str:"""执行工具并返回结果(字符串)"""if tool_name == "get_order_info":order_id = arguments.get("order_id")if not order_id:return "错误:缺少订单号"# 调用Java订单服务async with httpx.AsyncClient(timeout=10.0) as client:try:resp = await client.get(f"{ORDER_SERVICE_URL}/{order_id}")if resp.status_code == 200:data = resp.json()return f"订单{order_id}:金额{data['amount']}元,状态{data['status']},商品{data['product']}"else:return f"未找到订单{order_id}"except Exception as e:return f"查询订单失败:{str(e)}"elif tool_name == "get_weather":city = arguments.get("city")# 模拟天气API(可替换为真实API如wttr.in)# 这里简单返回固定数据weather_data = {"北京": "晴,25°C","上海": "多云,28°C","深圳": "雷阵雨,30°C"}return weather_data.get(city, f"未知城市{city},天气信息不可用")else:return f"未知工具:{tool_name}"
3.Fastapi定义接口
from fastapi import FastAPI, HTTPExceptionfrom fastapi.requests import Requestfrom fastapi.responses import JSONResponsefrom fastapi.responses import StreamingResponsefrom pydantic import BaseModelimport httpximport jsonfrom dotenv import load_dotenvfrom typing import List, Any, Dictfrom tools import TOOLS, execute_toolfrom config import ConfigParamclass Message(BaseModel):role: str # user, assistant, toolcontent: str = Nonetool_calls: Any = Nonetool_call_id: str = Noneclass ChatARequest(BaseModel):messages: List[Message] # 对话历史# 可以添加temperature等参数class ChatAResponse(BaseModel):reply: strmessages: List[Message] # 更新后的对话历史(包含assistant回复及可能的tool消息)# ---------- OpenAI 后端 ----------async def chat_with_openai(messages: List[Dict], tools=None):from openai import AsyncOpenAIclient = AsyncOpenAI(api_key=ConfigParam.MODEL_BASE_URL)response = await client.chat.completions.create(model=ConfigParam.LLM_MODEL,messages=messages,tools=tools,tool_choice="auto",temperature=0.2)return response# ---------- Ollama 后端 ----------async def chat_with_ollama(messages: List[Dict], tools=None):# Ollama 支持 tools 参数,格式与 OpenAI 类似baseUrl = ConfigParam.MODEL_BASE_URLif baseUrl.endswith('/'):baseUrl = baseUrl[:-1]url = f"{baseUrl}/api/chat"payload = {"model": ConfigParam.LLM_MODEL,"messages": messages,"stream": False,"options": {"temperature": 0.2}}if tools:payload["tools"] = tools # Ollama 0.3+ 支持async with httpx.AsyncClient(timeout=60.0) as client:print("Ollama request:", payload)resp = await client.post(url, json=payload)resp.raise_for_status()data = resp.json()print("Ollama response:", data)# 构造类似 OpenAI 的响应对象class MockChoice:class MockMessage:def __init__(self, d):self.content = d.get("content")self.tool_calls = d.get("tool_calls")def __init__(self, d):self.message = self.MockMessage(d["message"])class MockResponse:def __init__(self, d):self.choices = [MockChoice(d)]return MockResponse(data)# 选择后端LLM_BACKEND = "ollama"# ---------- 统一处理循环 ----------async def run_conversation(messages: List[Dict]) -> List[Dict]:"""处理tool calling循环,返回最终的messages列表(包含assistant最终回复)"""# 复制一份,避免修改原列表conversation = messages.copy()max_iter = 5 # 防止无限循环for _ in range(max_iter):# 调用LLMif LLM_BACKEND == "openai":response = await chat_with_openai(conversation, tools=TOOLS)else:response = await chat_with_ollama(conversation, tools=TOOLS)assistant_msg = response.choices[0].message# 将assistant消息加入对话assistant_dict = assistant_msg.model_dump() if LLM_BACKEND == "openai" else {"role": "assistant","content": assistant_msg.content,"tool_calls": assistant_msg.tool_calls}conversation.append(assistant_dict)# 如果没有tool_calls,结束循环if not assistant_msg.tool_calls:break# 有tool_calls,执行每个工具并追加tool消息for tool_call in assistant_msg.tool_calls:print(f"Received tool call: {tool_call}")tool_call = tool_call if isinstance(tool_call, dict) else tool_call.model_dump()tool_name = tool_call["function"]["name"]print(f"Received tool call: {tool_name}")arg = tool_call["function"]["arguments"]if isinstance(arg, str):arguments = json.loads(arg)else:arguments = argtool_result = await execute_tool(tool_name, arguments)print(f"Executed tool {tool_name} with args {arguments}, got result: {tool_result}")# 添加tool response消息conversation.append({"role": "tool","tool_call_id": tool_call["id"],"content": tool_result})return conversation@app.post("/chat_tool", response_model=ChatAResponse)async def chat(request: ChatARequest):# 将pydantic模型转为dict列表messages = [msg.model_dump(exclude_none=True) for msg in request.messages]try:updated_messages = await run_conversation(messages)# 提取最后一条assistant消息的content作为回复last_assistant = Nonefor msg in reversed(updated_messages):if msg.get("role") == "assistant" and msg.get("content"):last_assistant = msg["content"]break# 将updated_messages转回Message列表(简化,不要求完整还原tool_calls结构)# 直接返回字符串和完整历史(前端可以存储)return ChatAResponse(reply=last_assistant or "抱歉,没有生成回复。",messages=[Message(**m) for m in updated_messages if m.get("role") != "tool" or True] # 保留tool消息)except Exception as e:print("Error in /chat_tool:", str(e))raise HTTPException(status_code=500, detail=str(e))if __name__ == "__main__":import uvicornuvicorn.run(app, host="0.0.0.0", port=8000)
夜雨聆风