
Prompt 构建完成后,RenoPit 还要面对不同模型、不同兼容接口以及图片能力差异。开源仓库 fthux/RenoPit 将这些差异集中在 llm_service.py,业务层只调用 analyze_design() 等统一函数。
一、统一创建 OpenAI 兼容客户端
_get_openai_client() 使用三个环境配置创建 AsyncOpenAI:API Key、Base URL 和 180 秒请求超时。只要服务实现 OpenAI Chat Completions 兼容协议,RenoPit 就可以通过同一客户端调用。
设计分析、合同分析、增项预测和交叉核查最终都进入两个底层函数:
_call_llm():发送图片和文本;_call_llm_text():只发送系统 Prompt 与用户文本。
这种划分让多模态能力判断集中在设计分析路径,文档类任务始终使用纯文本调用。
二、图片如何进入多模态消息
_call_llm() 遍历 Base64 图片,识别可能存在的 Data URL 前缀,默认 MIME 类型为 image/jpeg,然后构造 OpenAI 的 image_url 内容:
python
content_parts.append({
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{img_data}",
"detail": "high",
},
})
全部图片之后再追加一个 text 内容块。最终请求包含一条 system 消息和一条 user 消息,其中 user 的 content 是图片块与文本块组成的数组。
模型名称完全来自 LLM_MODEL_NAME,最大输出设置为 65536 Token,温度为 0.3,让结构化结果保持相对稳定。
三、联网工具只对特定端点启用
当 ENABLE_WEB_SEARCH 为真时,代码还会检查 Base URL 是否属于 OpenAI API。只有满足条件才附加 web_search_preview 工具。
Gemini、DeepSeek、Ollama 等兼容端点不会收到该参数,因为它们可能能处理 Chat Completions,却不支持同样的工具定义。此时联网要求仍保留在系统 Prompt 中,API 请求本身保持普通聊天格式。
四、重试封装如何工作
多模态和纯文本各有一个指数退避封装。当前常量为:
python
MAX_RETRIES = 2
BASE_DELAY = 2
TIMEOUT_SECONDS = 180
每次调用都由 asyncio.wait_for() 限制在 180 秒内。空字符串也会被视为失败。第一次失败后等待两秒再进行第二次尝试;两次都失败时抛出带模型名称和最后错误信息的 RuntimeError。
多模态重试对 ModelNotMultimodalError 做了特殊处理:这种错误不会继续重复请求,而是立即交给上层决定是否走文本降级。
五、如何识别模型不支持图片
部分纯文本模型收到 image_url 后会返回包含 unknown variant image_url 的 400 错误。_call_llm() 捕获异常并把它转换成 ModelNotMultimodalError。
analyze_design() 先判断是否有图片和文本,再决定执行路线:
flowchart TD Aanalyze_design --> B{是否有图片} B -->|是| C多模态调用 C -->|成功| D返回 LLM 文本 C -->|不支持图片| E{是否还有文本} C -->|其他调用失败| E B -->|否| E E -->|有| F纯文本调用 E -->|无| G抛出 RuntimeError F --> D
如果用户同时上传了图片和合同,即使当前模型不支持视觉输入,系统仍能使用提取出的合同文本和用户说明完成分析。只有"存在图片、模型不支持图片、又没有任何文本"时才无法降级。
六、同步 Celery 任务如何调用异步函数
LLM 服务全部使用 async def,但 Celery 的 run_analysis_task 和分析引擎是同步函数。run_analysis_sync() 通常通过 asyncio.run() 执行 analyze_design();如果当前线程已经存在运行中的事件循环,则使用线程池创建新的异步执行环境。
文档分析、增项预测和交叉核查也采用同样的同步包装。因此异步网络客户端被限制在服务层,Celery 编排代码仍然保持同步调用形式。
七、LLM 服务调用链小结
RenoPit 的 LLM 层完成四件事:根据配置创建兼容客户端,把图片和文本组装成请求,为超时及空响应提供重试,并在模型缺少视觉能力时切换到纯文本。上层分析引擎拿到的始终是一段待解析的模型输出文本。
相关实现可以在 fthux/RenoPit 的 llm_service.py 中逐行对照。下一篇将分析这段原始文本如何经过 AnalysisEngine、JSON 修复和数据库事务,最终成为可查询的结构化装修闭坑结果。