做企业级AI系统,和写几个API调用Demo完全是两码事。我在把DeepSeek与Kimi API装进真实生产环境的过程中,踩过不少坑:同一个Prompt,一个模型理解得明明白白,另一个能答出完全相反的意思;单路模型限流一上来,线上工单直接堆积;再加上上下文窗口、密钥轮换、监控告警,不亲手趟一遍,根本不知道水有多深。这篇实战笔记把我从零搭建企业级AI系统的完整路径拆开讲:架构选型、API接入、工程化封装、真实问答系统落地,以及最常见的排障办法。适合刚拿到API Key准备动手集成的开发者,也适合已经能跑通接口,但还想把稳定性、成本和效果再打磨一轮的工程师。
1. 企业级AI系统搭建前的整体思考
1.1 别把“能调API”当成“能用”
企业级系统和个人Demo最大的区别,不是模型选得多好,而是“出了问题系统还能不能继续工作”。我个人Demo阶段习惯这么写:拿一个Key,拼一个Prompt,直接 requests.post,拿到JSON就完事。这套逻辑放到企业环境里,基本活不过第一周。
企业级要求的是可治理、可观测、可降级、可计量。所谓可治理,是所有模型调用必须走统一入口,密钥不能散落在各个服务里;可观测,是每一次请求都要能追溯到模型、Prompt版本、Token消耗、响应时长和失败原因;可降级,是DeepSeek挂了或者限流了,系统不会整个瘫掉,而是切到Kimi或返回话术;可计量,是每个部门、每个应用花了多少钱要能算清楚。
我常用下面这张表去说服团队,别把时间浪费在“能打通API”的兴奋点上:
| 维度 | 个人Demo | 企业级系统 |
|---|---|---|
| 调用方式 | 代码里直接发请求 | 统一模型网关,多模型路由 |
| 失败处理 | 报错就重新执行 | 重试、限流、熔断、降级 |
| Prompt | 字符串写死在函数里 | 模板管理、版本化、可审计 |
| 上下文 | 数组无限向后堆 | 窗口管理、摘要压缩、RAG检索 |
| 观测 | print打印结果 | 全链路日志、指标、告警 |
| 成本 | 不关心 | 按项目/部门计量,设置预算 |
1.2 DeepSeek与Kimi混用的架构逻辑
很多刚上手的人会问:既然要做企业级系统,为什么不用一个模型全搞定?答案很简单:没有哪个模型能在效果、成本、速度、上下文长度上同时做到最优。与其赌一个模型,不如把多个模型当成同一套网关里的两个成员,按场景路由。
在我自己的项目里,DeepSeek通常作为默认推理引擎。它在逻辑推理和代码相关任务上表现扎实,成本也相对容易控制,适合做日常问答和结构化输出。Kimi我更多用来做长文档解析和备选链路,它的长上下文能力在处理几十页PDF、多轮复杂历史时很有价值。两个模型不是替代关系,而是互补关系:默认走DeepSeek,遇到长文本、超长上下文或者DeepSeek临时限流时再切Kimi。
这样做还有一个隐藏好处:不绑定单一供应商。一旦某个模型调整定价或上线不稳定,系统可以平滑迁移流量,而不是被某一个外部服务卡住脖子。
1.3 核心组件与交互流程
一个可落地的企业级AI系统,我习惯分成五层:
- 接入层:负责接收业务请求,处理用户身份、会话ID、权限校验。
- 模型网关:负责统一调用DeepSeek和Kimi,做模型路由、重试、降级、限流。
- 编排层:负责组装Prompt、管理上下文、调用知识库检索、执行工具函数。
- 模型层:DeepSeek、Kimi等大模型服务,只做生成这一件事。
- 应用层:把AI能力封装成业务功能,比如工单助手、客服机器人、知识库问答。
交互流程可以简化成:业务请求进来后,先经过网关确认用哪个模型,再到编排层拼好上下文,然后调用模型生成候选答案,最后做格式化和落地。如果生成过程中触发限流,网关自动切到备用模型;如果备用模型也失败,就返回兜底话术并记录告警。这套逻辑听起来不复杂,但每一步都有很多细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从零接入DeepSeek与Kimi API
2.1 准入准备与鉴权配置
先说最基础的环境准备。企业级系统和个人脚本最大的不同,就是密钥不能写死在代码里。我见过有人把API Key传到Git仓库,结果被扫描工具抓个正着,不得不紧急轮换。正确的做法是放进环境变量或独立的密钥管理服务里。
调用层面,DeepSeek和Kimi都提供HTTP接口,并且兼容当前主流的大模型接口风格,所以直接用 requests 或 httpx 就能统一调用,不需要为每个模型维护一套独特逻辑。下面是一个最小客户端配置:
python复制import os
import requests
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")
KIMI_API_KEY = os.getenv("KIMI_API_KEY")
KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.moonshot.cn/v1")
def call_model(api_key, base_url, model, messages, temperature=0.3, max_tokens=1024, stream=False):
resp = requests.post(
f"{base_url.rstrip('/')}/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"stream": stream,
},
timeout=30,
)
resp.raise_for_status()
return resp.json()
这里有几个细节需要留意。base_url 后面加不加 /v1 会影响拼接地址,我建议统一从环境变量读取,不要写死在代码里。model 参数也不是凭感觉填,DeepSeek 常见的是 deepseek-chat,Kimi 这边通常是 moonshot-v1-8k 或类似的长短版本,最好登录控制台确认一下当前租户可用模型列表。
Key的权限也要分开管理。生产环境和测试环境用不同的Key,每个Key设置独立的配额和额度告警。密钥轮换时要支持“新旧并行”的过渡期,先在配置里加新Key,确认稳定后再移除旧的,避免一把梭导致线上中断。
2.2 调用参数与请求示例
拿到接口连通性之后,下一步就是理解请求参数。很多新手只照着Demo填参数,结果生成质量时好时坏。这里把最关键几个参数讲透。
temperature 控制随机性,范围一般是0到2。0意味着更确定,适合知识问答、信息抽取、格式转换;0.7到1.0适合创意写作、头脑风暴。做企业级系统时,我默认把内部知识库问答的 temperature 设置在0.1到0.3之间,减少“一本正经胡说八道”的概率。
top_p 是核采样,模型只从累计概率达到该阈值的候选词里选下一个词。它和 temperature 不是必须同时调。我的经验是:保持 top_p=1,只调 temperature 是最简单可控的组合。如果非要一起调,也不要两个都往中间调,容易让输出变得诡异。
max_tokens 限制的是生成Token数,不是总上下文长度。它只约束输出部分。如果一个模型能处理64K上下文,但 max_tokens 设置太小,长答案会在中途被截断,而且接口不一定报错,只会返回不完整的文本。
实际调用时,messages 数组的组成非常关键。一个典型的对话结构是:
python复制messages = [
{
"role": "system",
"content": "你是企业内部的智能助手。你的回答必须简洁、准确,禁止编造数字和事实。"
},
{
"role": "user",
"content": "请说明本月服务可用率是多少?"
},
{
"role": "assistant",
"content": "本月的服务可用率为99.95%,数据来源见运维周报。"
},
{
"role": "user",
"content": "那上个月呢?"
},
]
system 角色用来设定全局规则,user 和 assistant 的轮替则代表对话历史。多轮对话时,历史必须按顺序传给模型,不能乱插。如果用户身份或业务信息需要约束,也尽量放在 system 里,而不是塞进每一条用户消息。
如果希望让模型严格输出JSON,可以在请求里带上输出格式约束相关的参数,配合 temperature 调低,能大幅提高下游解析成功率。不要只靠“请输出JSON”这句话去赌模型心情。
2.3 流式输出与并发控制
企业应用里,等待完整JSON再渲染页面会让用户体验很差。用户在对话框里敲完问题,如果3秒内没有字冒出来,就会以为系统挂了。所以生产环境必须支持流式输出。
用 requests 做流式调用也不复杂,核心是设置 stream=True,然后逐行解析服务端返回的数据行:
python复制def stream_chat(api_key, base_url, model, messages, temperature=0.3):
resp = requests.post(
f"{base_url.rstrip('/')}/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
},
json={
"model": model,
"messages": messages,
"temperature": temperature,
"stream": True,
},
stream=True,
timeout=(5, 60),
)
resp.raise_for_status()
for line in resp.iter_lines():
if not line:
continue
if line.startswith(b"data:"):
data_str = line[5:].strip()
if data_str == b"[DONE]":
break
data = json.loads(data_str)
choices = data.get("choices", [])
if not choices:
continue
delta = choices[0].get("delta", {}).get("content")
if delta:
yield delta
流式返回的每个 chunk 里只包含一小段增量文本。后端拿到这些增量后,可以直接通过SSE逐段推给前端。注意超时时间要区分“连接超时”和“读取超时”,因为长文本生成可能持续几十秒,连接一旦建立就不能因为读取超时被断开。
并发控制同样重要。如果业务量上来,几十个请求同时打到一个模型上,很容易触发限流。我习惯用信号量或线程池限制单个API Key的并发数。比如用 asyncio.Semaphore(10),最多允许10个并发请求,超过的排队等待:
python复制import asyncio
from httpx import AsyncClient
sem = asyncio.Semaphore(10)
async def safe_call(client, model, messages):
async with sem:
resp = await client.post(
"/chat/completions",
json={"model": model, "messages": messages},
headers={"Authorization": f"Bearer {os.getenv('API_KEY')}"},
timeout=60,
)
return resp.json()
并发数不是越大越好,具体要看模型平台给出的配额限制。最好先在压测环境跑一遍,确认不同并发下的错误率和延迟,再设定一个安全阈值。
3. 从“能跑通”到“能上线”的工程化封装
3.1 Prompt工程与系统提示词管理
接口通了之后,大部分人第一个想优化的就是Prompt。我见过最普遍的做法,是把Prompt直接拼在业务代码里,比如 f"你是客服,用户的问题是:{question}"。短期看没问题,上线两周后就会很痛苦:运营改一句话,要动代码;不同渠道想用不同话术,只能复制函数;想统计某个Prompt版本的效果,根本做不到。
企业级的做法是让Prompt可配置、可版本化。系统提示词单独放在配置文件或管理后台里,代码里只引用模板标识。我给一个内部客服系统写的system prompt通常长这样:
code复制你是企业内部门户的智能助手。
规则:
1. 只能依据“参考资料”中提供的信息回答。
2. 如果参考资料不足以回答,明确说“资料库中未找到,请联系人工客服”。
3. 回答结构:先给结论,再给依据,最后给出建议。
4. 禁止编造订单编号、价格、政策条款等事实数据。
这段提示词稳定运行了很久。要点在于:角色定位清晰、行为边界明确、输出格式固定。不要试图用一段花哨的Prompt解决所有问题,规则越多,模型越容易顾此失彼。真正有效的做法,是用系统提示词把“不能做什么”框死,再用少样本示例把“应该做成什么样”演示出来。
对多轮对话,我还会在系统提示词里加一句话:如果用户问题在历史中已经回答过,优先基于历史结果回答,避免重复调用知识库检索。
3.2 上下文管理与知识库增强
企业级系统很快会撞上上下文窗口的墙。对话轮数少还好,一旦超过几十轮,把全部历史塞进 messages,Token成本直线上升,甚至直接超限。我的处理方法是分三步:
第一步,控制历史长度。用Token数量而不是轮数来控制窗口。每轮对话插入前,先估算当前总Token数,超过阈值就丢掉最老的非系统消息,保留更近的对话。
第二步,超长历史做摘要。如果对话横跨多个主题,直接丢老消息会丢失关键信息。这时可以在每N轮之后,调用一次模型把前面的历史压缩成摘要,再把摘要作为一条新的历史消息放回上下文。这种方式比硬截断效果稳定得多。
第三步,知识库检索增强。企业问题往往依赖内部资料,不可能全部塞进Prompt。正确做法是:用户问题进来后,先从向量数据库中检索TopK相关文档片段,再把片段拼到用户消息里让模型回答。一个简洁的实现片段:
python复制def build_rag_user_content(question, docs):
context = "\n\n".join(
f"【资料{i + 1}】{doc['content']}" for i, doc in enumerate(docs)
)
return f"请基于以下资料回答问题:\n\n{context}\n\n问题:{question}"
这里TopK我通常设3到5,不是越大越好。TopK太大会混入大量无关内容,模型反而不知道该信哪段。资料片段也要带上编号,并且要求模型在回答里标注“依据资料2”,这样人工审核时能快速核对来源。
3.3 函数调用与工具联动
只会“你问我答”的AI系统,价值有限。真正能落地的系统,往往需要AI调用业务系统,比如查询订单、查询库存、提交工单。这就要用到函数调用能力。
实现上不复杂:给模型声明一批可用的函数,包括函数名、描述和参数结构;模型根据用户问题决定是否调用,以及调用时填什么参数;系统收到调用请求后,执行对应函数并返回结果给模型,让模型基于结果生成最终回答。
示例函数定义如下:
python复制tools = [
{
"type": "function",
"function": {
"name": "query_order_status",
"description": "根据订单号查询订单状态",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "订单号"}
},
"required": ["order_id"]
}
}
}
]
请求时带上 tools=tools。如果模型返回 tool_calls,就把模型回复追加到 messages,再把实际函数的执行结果以 tool 角色的消息放回去,继续请求模型生成最终答案。这里最关键的一点是:工具描述要写清楚“什么情况下用”,不要只写“查询订单”,要写“只有用户提供订单号时调用;如果没订单号,先向用户询问”。
函数调用上线前,一定要在沙箱环境把参数校验做扎实。模型填的参数未必合法,业务系统不能直接信任。多一层模式校验,能少出很多线上事故。
3.4 限流、重试与熔断
企业级系统的稳定性,很大程度取决于失败处理策略。我之前带过一个项目,上线第一天就被模型平台的限流教育了:几十个用户同时提问,接口直接返回429,页面全白。后来我们加了限流、重试和熔断,才算真正稳定下来。
先说重试。不是所有错误都值得重试。网络超时、429限流、5xx服务端错误可以重试;400参数错误、401鉴权失败、403无权限,重试一万遍也没用。重试要用指数退避,不要用固定间隔。比如第一次等1秒,第二次2秒,第三次4秒,再加上随机抖动,避免所有请求在同一点撞车。
伪代码是这样的:
python复制def call_with_retry(func, retries=3):
for attempt in range(retries):
try:
return func()
except RateLimitError:
time.sleep(2 ** attempt + random.uniform(0, 1))
except ServerError:
time.sleep(2 ** attempt + random.uniform(0, 1))
# 最终失败,走降级
return fallback_response()
再说熔断。如果一段时间内连续失败超过阈值,比如60秒内错误率超过50%,那就不该继续疯狂重试了。这时候直接快速失败,把流量切到备用模型,或者返回“请稍后再试”的兜底文案。熔断恢复也要谨慎,一般等一个冷却期后再放开少量请求探活,探到成功再逐步恢复流量。
限流不仅是对模型平台的礼貌,也是对自己的保护。如果业务层不限制并发,一旦某个页面流量突增,底层模型接口会被打穿,所有用户一起遭殃。在网关层做好单Key并发控制和全局QPS控制,是上线前必须完成的动作。
4. 实战案例:企业级智能问答系统
4.1 需求边界与方案拆解
讲完基础,我用一个完整案例把这些内容串起来。假设某公司要做一个内部知识库智能问答系统:员工提问,系统基于企业制度文档回答,同时支持多轮追问,最后还能把不确定性高的问题转人工。
需求拆解下来就三件事:
- 检索:从文档库里找出与当前问题最相关的片段。
- 生成:把检索片段、系统提示词和对话历史组装好,调用DeepSeek生成回答。
- 降级:DeepSeek限流或失败时,切到Kimi;Kimi也失败时,返回转人工提示。
实现前要先定边界。这个系统不追求“什么都能答”,只回答有依据的问题。对没有资料支撑的问题,必须让模型明确说“不知道”,而不是强行编一个答案。这个边界写进系统提示词,也写进产品需求里,避免后期被无休止的“测试题”拖垮。
4.2 核心链路代码实现
我把核心流程写成一段可运行的代码骨架。它不依赖具体框架,只展示思路:
python复制class IntelligenceAnswerService:
def __init__(self, deepseek_key, kimi_key, vector_store, memory):
self.deepseek_key = deepseek_key
self.kimi_key = kimi_key
self.vector_store = vector_store
self.memory = memory
def build_system_prompt(self):
return (
"你是企业知识库智能助手。\n"
"1. 只依据资料回答,禁止编造。\n"
"2. 资料不足时,回答:资料库中未找到相关信息。\n"
"3. 每个结论后必须标注来源编号。"
)
def build_messages(self, user_id, question):
history = self.memory.get_history(user_id)[-10:]
docs = self.vector_store.search(question, top_k=5)
context = "\n\n".join(
f"【资料{i + 1}】{doc['content']}" for i, doc in enumerate(docs)
)
user_content = (
f"参考资料:\n{context}\n\n"
f"对话要求:基于参考资料回答,没有依据不要编造。\n\n"
f"当前问题:{question}"
)
messages = [{"role": "system", "content": self.build_system_prompt()}]
for turn in history:
messages.append({"role": "user", "content": turn["question"]})
messages.append({"role": "assistant", "content": turn["answer"]})
messages.append({"role": "user", "content": user_content})
return messages
def generate_answer(self, user_id, question):
messages = self.build_messages(user_id, question)
try:
return stream_chat(
self.deepseek_key, DEEPSEEK_BASE_URL,
"deepseek-chat", messages
)
except RateLimitError:
logger.warning("deepseek rate limited, switch to kimi")
return stream_chat(
self.kimi_key, KIMI_BASE_URL,
"kimi-api-model", messages
)
except Exception as e:
logger.exception("model call failed")
return iter(["系统繁忙,请转人工客服处理。"])
这套代码有几个关键点。history 里只存最近的10轮,是Token成本和质量之间的平衡。vector_store.search 返回的 docs 必须带编号,模型才能引用。降级逻辑放在异常边界处,如果DeepSeek任何环节出错,立刻切Kimi,不让用户感知到故障。
实际落地时,我会把模型名、温度、最大Token数、TopK都放到配置文件里,由运维和算法共同维护,不用每次改代码。
4.3 效果评测与灰度发布
系统写完之后,不能直接全量上线。首先要用一组合格测试集做离线评测。我的做法是从真实工单里挑出50到100个有标准答案的问题,标注正确答案后跑一遍,然后人工看四类指标:
| 指标 | 含义 | 及格参考 |
|---|---|---|
| 可回答率 | 模型是否返回了非空回答 | 100% |
| 引用准确率 | 引用的资料编号是否与实际依据匹配 | 稳定高于90% |
| 事实一致性 | 答案与标注答案是否矛盾 | 越低越好 |
| 转人工率 | 模型主动承认不会或转人工的比例 | 按业务目标定 |
线上灰度发布也不要一步到位。我常用的策略是按用户ID哈希分桶,比如先放10%流量到新系统,观察报错率和平均延迟,再逐步扩大到30%、50%、100%。灰度期间同时对比新旧逻辑的日志,如果新系统触发熔断次数明显偏高,就立刻回滚。
另外,日志里一定要记录 question、retrieved_docs_ids、answer、model、prompt_version、latency_ms 和 token_usage。这些字段是以后优化效果和排查问题的唯一线索。
5. 排障手册与经验教训
5.1 高频问题速查表
无论准备多充分,生产环境一定会出问题。下面这张表是我实际踩过的高频故障,基本覆盖了刚上线的常见情况:
| 表现 | 常见原因 | 处理建议 |
|---|---|---|
| 401 Unauthorized | Key错误、Key过期、环境变量未生效 | 检查环境变量,确保证书不过期,先本地打印Key掩码 |
| 429 Too Many Requests | 并发超限、配额不足 | 降低并发数,加指数退避重试,必要时切备用模型 |
| 400 Bad Request | messages结构错误、tools格式非法 | 用离线样例校验请求体,检查role字段和JSON结构 |
| 返回内容为空 | max_tokens太小、模型被系统提示词约束过度 | 调大max_tokens,检查finish_reason |
| 上下文超限 | 历史消息和检索片段加起来超过模型窗口 | 做截断、摘要,控制TopK |
| 回答明显跑偏 | 检索噪声太大、Prompt边界不清晰 | 缩小TopK,增加“不知道就说不知道”的规则 |
| 响应特别慢 | 非流式等待完整结果、并发争抢资源 | 开启流式输出,限制单Key并发 |
先说结论:排查顺序永远是“网络层、鉴权层、参数层、模型层”。先看接口返回码,再看有没有请求体,最后才怀疑模型效果。很多所谓“模型回答变笨了”的case,最后都查到是缓存失效或者环境变量被改掉了。
5.2 排查思路与避坑心得
我自己习惯把排查过程变成一套固定动作。出错时,先打开日志看 request_id 和状态码,把单次请求完整追踪一遍;再对比同一条Prompt在DeepSeek和Kimi上的表现,确认是模型问题还是策略问题;如果只在一个模型上失败,看是不是限流或模型配置差异。这样能避免在生产环境里靠猜。
再分享几个避坑心得。
第一,API Key永远不要出现在前端代码或日志里。前端调用应该走后端网关,由后端保存密钥。日志打印Key时要做掩码,比如只保留最后四位。这个问题最容易在团队协作时被忽略。
第二,不要盲目相信模型返回的JSON结构。即使加了格式约束,也可能出现字段缺失、类型不对。所有大模型输出都要做一层校验和容错,解析失败时提供“系统暂时无法解析,请重试”的兜底回复。
第三,对不同模型做能力隔离。DeepSeek和Kimi可能在某些字段含义上有细微差异,比如 usage 里的Token口径、流式结束标记。统一用网关层做适配,业务代码不要接触到模型差异。
第四,评估Prompt时不要只看一次回答。我有一门“坏习惯”:改一个Prompt后,拿5到10个测试题跑一遍,看差距。但注意测试题应该长期固定,不能每次换题,不然你永远不知道这个版本是变好还是变差。
最后还有一点:企业级AI系统的技术选型不要迷信“最强模型”,而要迷信“可替换的模型网关”。DeepSeek和Kimi都很好,但真正让系统稳定的,是你拥有“切走”的能力。多层路由、降级、监控和灰度,比单次调用本身更重要。
我刚开始做这套系统时,总想着把最高级的Prompt和最大上下文一次配上,结果上线第一天就遇到了上下文超限和限流双杀。后来把复杂度收回到“网关、编排、数据”三层里,每个模型只负责自己最擅长的事,系统才真正稳定下来。如果你也准备拿DeepSeek和Kimi API做企业级应用,先把最小闭环跑通,再把日志、重试、降级这三件事补齐,然后再谈优化。这一篇可以收藏,但比收藏更重要的是把第一行代码跑起来。
