agent学习Day7——依赖注入、中间件与 LLM 服务化
2026/7/17 21:31:39 网站建设 项目流程

一、依赖注入(Depends):让依赖不再写死

为什么需要依赖注入

之前项目里analyze_jd()这类能力和配置是"绑死"的:谁要用,谁就from app.core.config import settings自己读一遍.env,甚至函数内部直接import写死配置。问题有三:

  • 重复浪费:每个路由都自己读配置、自己初始化,代码抄好几遍。
  • 耦合:函数把"读配置"和"做业务"搅在一起,一处出错全链路连坐。
  • 没法测:配置写死在模块里,想换成假配置只能 mock 整个模块,很脏。

FastAPI 的Depends把这件事翻转过来——依赖不再自己import,而是别人喂进来(外部注入)

怎么用

写一个普通函数返回依赖对象,路由参数声明Depends(...),请求进来时 FastAPI 自动调用该函数并把返回值塞进参数:

# app/api/dependencies.pyfromapp.core.configimportSettings,settingsdefget_settings()->Settings:returnsettingsdefget_llm_service()->LlmService:returnLlmService(settings)
# app/main.py 的 /healthfromapp.api.dependenciesimportget_settingsfromapp.core.configimportSettingsdefhealth(settings:Settings=Depends(get_settings)):returnHealthResponse(app_name=settings.APP_NAME,...)

关键注意点

settings类型注解需要写Settings类(大写),而实际传入的值由Depends(get_settings)注入,因此main.py里只需from app.core.config import Settings,不必再import settings实例——类型与实例来源是两回事,不要混为一谈。

二、中间件(Middleware):给所有请求统一"插一脚"

为什么需要中间件

想记录每个请求花了多久,如果在每个路由里手写time.time(),重复代码一堆,漏写一个就缺数据。中间件是 FastAPI 给的"请求前后自动执行一段代码"的机制——它包在所有路由外面,每个请求都经过。

怎么写

# app/api/middleware.pyimportloggingimporttime logger=logging.getLogger(__name__)asyncdeflog_request_time(request,call_next):start=time.time()response=awaitcall_next(request)# 真正执行路由duration_ms=(time.time()-start)*1000logger.info(f"{request.method}{request.url.path}took{duration_ms:.2f}ms")returnresponse
# app/main.py,在 app = FastAPI() 之后注册fromapp.api.middlewareimportlog_request_time app.middleware("http")(log_request_time)

执行顺序:请求进 → 记开始时间 → call_next 跑路由 → 拿到 response → 算耗时打日志 → 返回

关键注意点

注意日志不打印的问题:代码明明执行了,终端却看不到耗时日志。根因是 Pythonlogging默认级别是WARNING,而中间件用logger.info(...)INFO级别低于WARNING会被直接吞掉;更隐蔽的是,logging.basicConfig(...)会被uvicorn 启动时覆盖,root logger 级别被抬回WARNING。修复方式是显式设置 root logger 并兜底添加 handler:

importlogging root_logger=logging.getLogger()root_logger.setLevel(logging.INFO)ifnotroot_logger.handlers:handler=logging.StreamHandler()handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))root_logger.addHandler(handler)

三、从函数到类:LLM 服务化(LlmService)

为什么改成类

之前的call_llm()是模块级函数,client = OpenAI(...)在模块被 import 的那一刻就执行了——只要.env里 key 少一行,任何 import 它的路由一加载就崩,/health都起不来。改成LlmService类,带来三层好处:

  • 操作与数据绑定settings存成self.settings,方法随用随取,不用每次拎参数。
  • 可测试__init__(settings)外部传入,测试时LlmService(fake_settings)一行搞定,呼应前面的Depends注入思想。
  • 延迟创建client挪进__init__,只有真实例化时才建连接,模块能正常 import,不再连坐崩溃。

改造后的骨架

classLlmService:def__init__(self,settings:Settings):self.settings=settings self.client=OpenAI(api_key=settings.DEEPSEEK_API_KEY,base_url=settings.DEEPSEEK_BASE_URL,)defcall_llm(self,prompt:str,system_prompt:str="",temperature:float|None=None)->str:iftemperatureisNone:temperature=self.settings.LLM_TEMPERATURE# ... 用 self.client / self.settings.LLM_MODEL / MAX_TOKENS / TIMEOUT ...returncontentdefanalyze_jd(self,jd_text:str)->JdAnalysisResult:user_prompt=build_jd_user_prompt(jd_text)raw=self.call_llm(prompt=user_prompt,system_prompt=SYSTEM_PROMPT_JD,temperature=0)returnparse_jd_analysis(raw)

同时给config.py补了LLM_TEMPERATURE/LLM_MAX_TOKENS/LLM_TIMEOUT三个字段,让调用参数全部来自配置而非硬编码。

路由层接入

新接口/api/v1/jd/analyze-basic-llm只干三件事:收请求 → 注入的llm_service.analyze_jd()→ 返回JdAnalysisResultllm_service不是import死的,而是Depends(get_llm_service)注入的:

@router.post("/analyze-basic-llm",response_model=JdAnalysisResult)defanalyze_basic_llm(req:JdAnalyzeRequest,llm_service:LlmService=Depends(get_llm_service),):returnllm_service.analyze_jd(req.jd_text)

四、pytest + dependency_overrides:用测试证明注入有用

为什么写这个测试

依赖注入的"可替换"好处不能只靠嘴说,要测试证明。FastAPI 给了一把钥匙:app.dependency_overrides

fromfastapi.testclientimportTestClientfromapp.api.dependenciesimportget_settingsfromapp.core.configimportSettingsfromapp.mainimportappdeffake_get_settings():returnSettings(APP_NAME="Test App",DEBUG=True)deftest_health_with_fake_settings():app.dependency_overrides[get_settings]=fake_get_settingstry:client=TestClient(app)response=client.get("/health")assertresponse.status_code==200data=response.json()assertdata["app_name"]=="Test App"assertdata["debug"]isTruefinally:app.dependency_overrides.clear()# 必须清理,否则污染其他测试

断言app_name == "Test App"通过,证明没动.env也换成了假配置。TestClient(app)要在dependency_overrides设置之后创建,否则覆盖不生效。

技术总结

把散落的配置与能力收拢成可注入、可覆盖的依赖,用中间件统一处理横切逻辑,用类把 LLM 客户端封装成延迟初始化、可测试的服务——这四个实践彼此呼应,共同解决的是同一个工程命题:让代码从"能跑"走向"可控、可测、可维护"。依赖注入是地基,中间件是横切面的统一入口,服务化封装消除了启动期脆弱性,而覆盖测试则为前三者提供了可信保障。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询