refactor: split streaming endpoint from Azure Functions

Disable /search/stream in Function runtime with a 501 guidance response, keep streaming on always-on FastAPI service, and update README with the new deployment and routing model.

Made-with: Cursor
This commit is contained in:
TetrisGGBOBot
2026-03-31 13:48:51 +00:00
parent 2718036501
commit bd8698f447
3 changed files with 39 additions and 7 deletions
+18 -4
View File
@@ -103,9 +103,10 @@ uvicorn serve:app --host 0.0.0.0 --port 8080 --reload
|------|------|------|
| GET | `/health` | 健康检查(本地 FastAPI) |
| POST | `/search` | 同步搜索(阻塞式,本地 FastAPI) |
| POST | `/search/stream` | 流式搜索(SSE 事件,本地 FastAPI) |
| POST | `/search/stream` | 流式搜索(SSE 事件,仅常驻 FastAPI 服务) |
> Azure Functions 部署时由 `function_app.py` 挂载到 `/api` 前缀,对外路径分别为 `/api/health`、`/api/search`、`/api/search/stream`。
> Azure Functions 部署时由 `function_app.py` 挂载到 `/api` 前缀,仅提供 `/api/health`、`/api/search`。
> `/search/stream` 必须走常驻 `uvicorn` 服务(避免 Functions 网关缓冲导致假流式)。
### 1. 健康检查
@@ -144,10 +145,10 @@ Jina 搜索结果固定上限:
- `cache_hit` - 是否缓存命中
- `audit_id` - 审计追踪 ID
### 3. SSE 流式搜索
### 3. SSE 流式搜索(常驻 FastAPI)
```bash
curl -N -X POST https://aisousuo.azurewebsites.net/api/search/stream \
curl -N -X POST https://your-stream-service/search/stream \
-H "Content-Type: application/json" \
-d '{"query":"PydanticAI 适合哪些场景?","search_mode":"deep"}'
```
@@ -157,6 +158,8 @@ curl -N -X POST https://aisousuo.azurewebsites.net/api/search/stream \
> 当请求超时时,SSE 会发送 `timeout` 事件并结束流。
> 如果你误调 Azure Functions 的 `/api/search/stream`,会返回 `501`,并提示使用独立流式服务地址。
---
## 环境变量配置
@@ -233,6 +236,17 @@ func azure functionapp publish aisousuo --python
curl https://aisousuo.azurewebsites.net/api/health
```
### 流式服务部署(推荐 App Service / Container)
```bash
# 启动常驻服务(示例)
uvicorn serve:app --host 0.0.0.0 --port 8080
```
- 将前端流式请求改为 `https://<stream-service>/search/stream`
- Azure Functions 继续承载同步接口:`/api/search`
- 可在 Functions 环境变量中配置 `STREAM_SERVICE_BASE_URL=https://<stream-service>`,用于错误提示回传目标流式地址
---
## 设计架构
+18 -2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import json
import os
from fastapi import FastAPI
from fastapi.responses import JSONResponse
@@ -13,7 +14,7 @@ from .config import Settings
from .models import SearchRequest, SearchResponse
def create_app() -> FastAPI:
def create_app(enable_streaming: bool = True) -> FastAPI:
settings = Settings.from_env()
agent = AISearchAgent(settings)
audit_logger = AuditLogger(settings.audit_log_path)
@@ -44,7 +45,22 @@ def create_app() -> FastAPI:
return JSONResponse(content=response.model_dump())
@app.post("/search/stream")
async def search_stream(request: SearchRequest) -> StreamingResponse:
async def search_stream(request: SearchRequest):
if not enable_streaming:
stream_base_url = os.getenv("STREAM_SERVICE_BASE_URL", "").rstrip("/")
return JSONResponse(
status_code=501,
content={
"error": "STREAMING_NOT_AVAILABLE_IN_FUNCTIONS",
"message": "Use the always-on stream service for /search/stream.",
"stream_endpoint": (
f"{stream_base_url}/search/stream" if stream_base_url else None
),
"query": request.query,
"search_mode": agent.resolve_search_mode(request),
},
)
async def event_generator():
# Send an immediate event so clients/proxies flush the SSE channel early.
yield "event: connected\ndata: {}\n\n"
+3 -1
View File
@@ -6,7 +6,9 @@ from starlette.routing import Mount
from ai_search_agent.api import create_app
_fastapi = create_app()
# Azure Functions keeps only synchronous search endpoints.
# Stream endpoint should be served by always-on uvicorn service.
_fastapi = create_app(enable_streaming=False)
# Azure Functions delivers the full path including /api/ prefix to the ASGI app.
# Mount FastAPI under /api so that /api/health -> FastAPI /health, etc.