1. 模型
在项目中,我们通过 langchain-openai 调用 DeepSeek 的 OpenAI 兼容接口。模型配置可以拆成连接信息、默认生成参数和允许使用的模型:
我们之前选择了在 .env 中配置模型参数,具体使用时,可以根据个人喜好选择
01[model]02provider = "openai"03name = "deepseek-v4-flash"04base_url = "https://api.deepseek.com"05temperature = 0.206max_tokens = 204807timeout_seconds = 3008max_retries = 209allowed_models = [10"deepseek-v4-flash",11"deepseek-v4-pro",12]
这些字段的含义如下:
| 字段 | 含义 | 是否通常固定在启动配置 |
|---|---|---|
provider | LangChain 使用哪种供应商集成 | 是 |
name | 传给模型接口的模型标识 | 可以是默认值 |
base_url | API 基础地址 | 是 |
temperature | 输出随机程度 | 可以允许受控覆盖 |
max_tokens | 单次输出上限 | 可以允许受控覆盖 |
timeout_seconds | 等待上游响应的超时 | 是 |
max_retries | 可重试错误的最大重试次数 | 是 |
allowed_models | 服务端允许调用的模型列表 | 是 |
provider 与模型名称不是一回事。使用 OpenAI 兼容接口时,LangChain 仍然需要 OpenAI 集成来组织请求,但 name 会原样交给实际服务商:
01from langchain_openai import ChatOpenAI0203from app.core.settings import Settings040506def create_model(settings: Settings) -> ChatOpenAI:07api_key = settings.model.api_key08if api_key is None:09raise RuntimeError("模型 API Key 未配置")1011return ChatOpenAI(12model=settings.model.name,13api_key=api_key.get_secret_value(),14base_url=str(settings.model.base_url),15temperature=settings.model.temperature,16max_tokens=settings.model.max_tokens,17timeout=settings.model.timeout_seconds,18max_retries=settings.model.max_retries,19)
ChatOpenAI 封装的是消息转换、HTTP 调用、流式输出、重试和响应对象构造等客户端逻辑。base_url 指向 DeepSeek 后,请求实际发送给 DeepSeek,不会因为类名中有 OpenAI 就改成调用 OpenAI 官方服务。
不同服务商对兼容参数的支持并不完全一致。TOML 能成功解析、LangChain 构造函数能接收参数,不代表上游一定会按相同方式执行。模型是否支持温度、工具调用、结构化输出、推理参数和流式 usage,需要以当前服务商文档与真实测试为准。
LangChain 也提供了专门的 langchain-deepseek 集成。只需要兼容接口中的基础文本能力时,可以继续使用 ChatOpenAI;如果要读取 reasoning_content 等 DeepSeek 扩展字段,或者使用供应商特有能力,应优先选择 ChatDeepSeek。此时还要安装 langchain-deepseek,并把 provider、模型工厂和依赖声明一起调整,不能只替换类名。
2. Settings
模型 Settings 可以使用 Pydantic 校验范围:
01from typing import Literal0203from pydantic import BaseModel, Field, HttpUrl, SecretStr, model_validator040506class ModelSettings(BaseModel):07provider: Literal["openai"] = "openai"08name: str = Field(min_length=1)09base_url: HttpUrl10api_key: SecretStr | None = None11temperature: float = Field(default=0.2, ge=0, le=2)12max_tokens: int = Field(default=2048, gt=0, le=100_000)13timeout_seconds: float = Field(default=30, gt=0, le=300)14max_retries: int = Field(default=2, ge=0, le=10)15allowed_models: list[str] = Field(min_length=1)1617@model_validator(mode="after")18def validate_default_model(self) -> "ModelSettings":19if self.name not in self.allowed_models:20raise ValueError("默认模型必须位于 allowed_models 中")21return self
这里把 provider 限制为当前项目真正接入的 openai,这样拼写错误会在启动时暴露。max_tokens 的 100_000 也是项目为了控制成本和响应时间设定的上限,不代表 DeepSeek 接口本身的最大输出能力。
API Key 仍然通过环境提供:
1MODEL__API_KEY=your-api-key
不要在 config.toml 中写真实密钥:
1# 不要提交这种配置2[model]3api_key = "sk-real-secret"
配置中的 name 是默认模型,allowed_models 是服务端允许列表。用户可以请求切换模型,但不能把任意字符串直接传给供应商:
1[model]2name = "deepseek-v4-flash"3allowed_models = [4"deepseek-v4-flash",5"deepseek-v4-pro",6]
1from app.core.settings import Settings234def resolve_model(requested: str | None, settings: Settings) -> str:5model = requested or settings.model.name6if model not in settings.model.allowed_models:7raise ValueError(f"不支持的模型:{model}")8return model
如果不同模型使用不同 base_url 或 API Key,仅切换模型名不够。此时应配置完整模型档案,并在服务端按受控 ID 选择,不能让客户端提交任意 URL 和密钥。
3. 动态参数
配置要区分进程级配置、业务输入、运行上下文和生成参数:
| 类别 | 示例 |
|---|---|
| 进程级配置 | base_url、API Key、HTTP 超时、最大重试、连接池 |
| 业务输入 | 用户当前发送的问题 |
| 运行上下文 | 会话 thread_id、用户身份和请求 ID |
| 生成参数 | 允许范围内的模型、温度、输出长度和输出格式 |
进程级配置适合在应用启动时创建模型客户端。其余内容随请求变化,不应通过修改全局 Settings 实现,否则两个并发请求可能互相覆盖。
LangChain 模型可以通过 bind() 派生本次调用参数:
01from collections.abc import Sequence0203from langchain_core.messages import BaseMessage04from langchain_openai import ChatOpenAI050607async def reply(08model: ChatOpenAI,09messages: Sequence[BaseMessage],10temperature: float,11) -> str:12request_model = model.bind(temperature=temperature)13response = await request_model.ainvoke(messages)14return response.text
bind() 返回一个带有本次调用参数的新 Runnable,不会修改 TOML,也不会改写共享模型实例。请求参数仍需通过 Pydantic Schema 限制范围。
LangChain 的 init_chat_model() 还支持 configurable fields,让调用时通过 Runnable config 切换受控字段。它适合一个图中需要声明式切换模型的场景,但不要把所有字段都暴露为 configurable:
01from langchain.chat_models import init_chat_model0203from app.core.settings import Settings040506def create_configurable_model(settings: Settings):07api_key = settings.model.api_key08if api_key is None:09raise RuntimeError("模型 API Key 未配置")1011return init_chat_model(12model=settings.model.name,13model_provider=settings.model.provider,14base_url=str(settings.model.base_url),15api_key=api_key.get_secret_value(),16configurable_fields=("model", "temperature", "max_tokens"),17config_prefix="chat",18)
调用时传入的字段仍应由服务端白名单生成:
01response = await model.ainvoke(02messages,03config={04"configurable": {05"chat_model": selected_model,06"chat_temperature": temperature,07"chat_max_tokens": max_tokens,08},09},10)
Runnable config 是一次调用的上下文,不等于应用 TOML。前者随请求变化,后者在进程启动时确定默认策略。
4. LangGraph
LangGraph 配置可以分为图构建参数、持久化参数和每次运行参数:
1[langgraph]2recursion_limit = 503checkpointer = "memory"4store = "memory"56[langgraph.postgres]7pool_size = 108pool_timeout_seconds = 30
各字段职责:
| 字段 | 作用 |
|---|---|
recursion_limit | 限制一次图运行最多继续多少步,防止错误循环无限执行 |
checkpointer | 保存每个 thread 的图状态与检查点 |
store | 保存可跨 thread 查询的长期数据 |
pool_size | 生产数据库连接池容量 |
pool_timeout_seconds | 获取数据库连接的等待上限 |
checkpointer 与 Store 不相同:
1checkpointer:保存一次会话 thread 内的状态、步骤和恢复点2store:保存跨 thread 使用的长期记忆或共享数据
开发和测试可以使用内存实现。下面的示例同时补全了最小图结构,model 会在节点中真正参与调用:
01from langchain_core.language_models import BaseChatModel02from langgraph.checkpoint.memory import InMemorySaver03from langgraph.graph import END, START, MessagesState, StateGraph04from langgraph.store.memory import InMemoryStore050607def create_memory_graph(model: BaseChatModel):08async def call_model(state: MessagesState):09response = await model.ainvoke(state["messages"])10return {"messages": [response]}1112builder = StateGraph(MessagesState)13builder.add_node("model", call_model)14builder.add_edge(START, "model")15builder.add_edge("model", END)1617checkpointer = InMemorySaver()18store = InMemoryStore()1920return builder.compile(21checkpointer=checkpointer,22store=store,23)
内存状态只存在于当前进程。重启会丢失,多 worker 之间也不共享,因此不适合需要可靠恢复的生产服务。这段函数固定创建内存实现,不会读取 TOML;在完整项目中,还需要由资源工厂读取 settings.langgraph.checkpointer 和 settings.langgraph.store,再选择对应实现。只修改配置字段,并不会自动切换后端。
生产环境可以在 TOML 中选择 PostgreSQL 后端,但连接 URL 仍通过敏感环境变量提供。双下划线表示嵌套层级,因此下面的变量对应 settings.langgraph.postgres.url:
1LANGGRAPH__POSTGRES__URL=postgresql://user:password@db/agent
图编译时注入 checkpointer。每次调用时提供 thread_id 和递归限制:
1result = await graph.ainvoke(2{"messages": messages},3config={4"configurable": {5"thread_id": conversation_id,6},7"recursion_limit": settings.langgraph.recursion_limit,8},9)
thread_id 是业务会话标识,必须来自当前已授权用户可访问的会话。不能让攻击者猜一个 ID 就读取其他用户的图状态。配置文件只能提供命名规则或默认策略,不能代替数据库授权检查。
5. 追踪
LangSmith tracing 常通过环境变量启用:
1LANGSMITH_TRACING=true2LANGSMITH_API_KEY=your-langsmith-key3LANGSMITH_PROJECT=fastapi-langchain-development4LANGSMITH_TRACING_SAMPLING_RATE=0.1
TOML 可以保存非敏感策略:
1[observability]2tracing_enabled = false3project = "fastapi-langchain-development"4sampling_rate = 0.1
但是,仅写 TOML 不会自动启用 LangSmith。sampling_rate 也不是 LangSmith 会自动查找的 TOML 字段。部署平台可以直接提供上面的 LangSmith 环境变量;如果项目把非敏感策略统一放在 TOML 中,就要在创建模型、Agent 或图之前完成一次显式映射:
01import os0203from app.core.settings import Settings040506def configure_langsmith(settings: Settings) -> None:07observability = settings.observability08os.environ["LANGSMITH_TRACING"] = str(09observability.tracing_enabled,10).lower()11os.environ["LANGSMITH_PROJECT"] = observability.project12os.environ["LANGSMITH_TRACING_SAMPLING_RATE"] = str(13observability.sampling_rate,14)
这段代码只映射公开策略,LANGSMITH_API_KEY 仍由运行环境提供。若项目改为手动创建 LangSmith Client,也可以把采样率传给 tracing_sampling_rate,再通过 tracing context 使用该 Client;两种接入方式选择一种即可。
追踪配置需要按环境管理:
| 环境 | 建议 |
|---|---|
| test | 默认关闭,避免上传测试数据 |
| development | 开发者明确开启 |
| staging | 可以较高采样,验证完整链路 |
| production | 根据隐私、成本和排障需求设置采样 |
LangSmith API Key 必须由环境提供。项目名称可以进入 TOML,但生产环境仍可覆盖。
追踪可能包含提示词、工具输入输出、模型回答和用户数据。启用前要确认脱敏、访问控制、保留时间与隐私策略。SecretStr 只保护配置对象打印,不会自动从 tracing 中移除用户内容。
6. 部署
本章讲的是 Python 应用如何使用 TOML 管理自己的运行配置。LangGraph CLI 和 LangSmith Deployment 还有独立的 langgraph.json:
1{2"dependencies": ["."],3"graphs": {4"assistant": "./app/graph.py:graph"5},6"env": "./.env"7}
它告诉 LangGraph 工具:
- 应安装哪些项目依赖;
- 哪些图需要对外提供;
- 图对象或工厂函数位于哪个模块;
- 本地开发从哪里读取环境变量。
langgraph.json 与 pyproject.toml 可以同时存在:
1pyproject.toml -> Python 依赖和工具配置2config.toml -> 我们的应用默认策略3.env -> 本地密钥和环境覆盖4langgraph.json -> LangGraph CLI/部署的图入口配置
不能因为喜欢 TOML 就把 langgraph.json 改成 langgraph.toml,LangGraph CLI 默认不会识别自创格式。也不要在 langgraph.json 的 env 字段中写密钥值,它应指向环境文件或由部署平台提供环境变量。
如果当前项目只通过 FastAPI 自己托管编译图,不使用 LangGraph CLI/Agent Server,就不必提前创建 langgraph.json。是否需要它取决于部署方式。
启动校验
AI 配置不仅要检查类型,还要检查组合关系:
01from typing import Literal0203from pydantic import BaseModel, Field, SecretStr, model_validator040506class PostgresSettings(BaseModel):07url: SecretStr | None = None08pool_size: int = Field(default=10, gt=0)09pool_timeout_seconds: float = Field(default=30, gt=0)101112class LangGraphSettings(BaseModel):13checkpointer: Literal["memory", "postgres"] = "memory"14store: Literal["memory", "postgres"] = "memory"15recursion_limit: int = Field(default=50, gt=0, le=500)16postgres: PostgresSettings = Field(default_factory=PostgresSettings)1718@model_validator(mode="after")19def validate_backend(self) -> "LangGraphSettings":20uses_postgres = (21self.checkpointer == "postgres" or self.store == "postgres"22)23if uses_postgres and self.postgres.url is None:24raise ValueError("PostgreSQL 后端需要数据库 URL")25return self
生产启动还应执行有超时的连通性检查,但不要让检查产生昂贵模型回答。可以验证:
- 数据库能建立连接并执行轻量查询;
- checkpointer 所需表是否准备完成;
- 模型凭据是否存在;
- 必要时执行极小的模型连接测试;
- tracing 开启时 LangSmith 配置是否完整。
失败信息应指出哪个组件不可用,但不能包含 API Key、数据库密码或完整 Authorization 头。
重试也要有边界。配置 max_retries = 10 可能把一次不可恢复的配置错误拖成很长超时。认证失败、模型不存在等客户端错误通常不应反复重试;网络抖动和限流才可能适合退避重试。