上个月接了一个内部工具改造任务,要把团队沿用已久的“代码问答”工作流升级成真正能独立干活的 AI coding agent。调研了一圈,最终选了 DeepSeek 作为底层模型,直接通过官方 API 对接进现有的 CLI 和 IDE 流程,跑通了从需求拆解、自动改代码到执行 checklist 的完整闭环。整个过程比想象中复杂,但也比想象中可控。这篇博客就是这次改造的全程记录,重点拆解“原生 AI coding agent”到底意味着什么,DeepSeek 在其中的能力边界在哪里,以及你在接入时会遇到的几个高频坑——尤其是 tool call 时序、上下文膨胀和超时重试。如果你正在考虑把代码助手升级成 Agent 模式,或者想接 DeepSeek 但又不想走第三方中转,这篇文章应该能省你不少时间。
适合三类人看:后端开发想自己搭一个 coding agent 的,团队技术负责人想评估 DeepSeek 做基座模型的,以及已经被 Codex、Claude 等 Agent 工具折腾过、想试试开源模型方案的人。下文所有参数和代码都是可直接使用的版本,我会把为什么这么配也讲清楚。
1. 项目整体设计与思路拆解
1.1 从“聊天机器人”到“Agent”的能力跃迁
很多人把 AI 编程助手和 coding agent 混为一谈,但这两者差别非常大。聊天机器人是“你问一句、它答一句”,核心是单轮或多轮对话;coding agent 则是“你给它一个目标,它自己规划步骤、调用工具、读取文件、执行命令、检查结果,直到任务完成”。前者是顾问,后者是拿你工资去跑腿的实习生。
要把 DeepSeek 从“问答工具”改造成“agent”,背后依赖三件事:
- 工具调用(function calling):模型不再是只会吐文字,而是能在回复里输出一个结构化的“函数调用请求”,由你的代码去执行这个函数,再把结果交还给模型继续推理。
- 长上下文保持:agent 要在一个会话里反复读取信息、修改代码、查看输出,如果上下文窗口太小,跑不了几步就“失忆”了。
- 自主规划与循环控制:agent 不是一次性响应的逻辑,而是一个循环:发请求→模型决定调用哪个工具→执行工具→把结果返回给模型→继续推理→直到模型认为任务完成。
DeepSeek 对这三件事的支持,实测下来是可用的:官方 API 支持丢tools参数,返回的tool_calls结构符合 OpenAI 兼容规范;deepseek-chat的上下文窗口是 64K,对一个 coding agent 的中等规模项目够用;流式输出也稳定,不会频繁断流。这就构成了“原生”的第一层基础能力。
1.2 “原生”到底意味着什么
“原生”这个词在被各种包装文案用烂之后,反而容易被误解。在这个项目里,我理解的原生有三层含义。
第一层是原生接口。DeepSeek 官方 API 直接兼容 OpenAI 的 chat completions 格式,不需要任何中转网关、代理层或专用 SDK。你用惯了 openai 库,换一个base_url和api_key就能切过去。这一点在实际工程里价值极大:团队里已有的代码、工具链、监控脚本几乎不用改动。
第二层是原生能力。DeepSeek 模型本身在预训练阶段就对代码理解、指令跟随和函数调用做了对齐,不是靠一堆提示词硬撑出来的。举个例子,我在测试里直接丢给它一个结构复杂的 JSON Schema 工具定义,它能准确生成符合参数的调用;而在某些开源模型上,同样的定义经常被模型当成普通文本忽略掉。这就是“原生”和“硬凑”的分水岭。
第三层是原生生态。现在 Codex CLI、Cline、Continue 这些主流 agent 工具都允许自定义模型提供方,DeepSeek 因为 API 兼容,几十秒就能被这些工具“接管”作为后端模型。等于你不用自己造轮子,直接在前人的 agent 框架上换引擎。
强调“原生”还有一个实际原因:我们团队之前用过第三方中转封装,延迟抖动、API 密钥泄漏风险、限流不稳定都踩过。直接对接官方 API 后,这些问题基本消失,出问题至少知道去哪查。
2. 模型选型:为什么 DeepSeek 适合做 coding agent 底座
2.1 能力、成本与生态的三重对比
坦白讲,DeepSeek 不是所有维度都最强,但在 coding agent 这个场景里,它刚好踩中了性价比甜点。我列了一张选型时做过的对比表(以实测感受和公开信息为准,价格随时可能有调整):
| 对比项 | DeepSeek(deepseek-chat) | OpenAI GPT-4o mini | 本地开源模型(R1 蒸馏版) |
|---|---|---|---|
| 上下文窗口 | 64K | 128K | 取决于部署配置 |
| 函数调用 | 原生支持、格式规范 | 原生支持 | 参差不齐 |
| 中文代码理解 | 好 | 良 | 因模型而异 |
| 单次请求成本 | 很低 | 中等 | 硬件成本为主 |
| 数据是否出网 | 出网 | 出网 | 不出网 |
| 接入复杂度 | 低(兼容 OpenAI) | 低 | 中 |
选 DeepSeek 还有一个大家容易忽略的原因:它的 token 计价里,输入和输出的价格差距不像某些大模型那么夸张。coding agent 的典型特征是“输入远大于输出”——每次请求都要把历史对话、工具结果、文件内容全塞进去,输出往往只有一小段修改。如果一个模型输出贵得离谱,agent 一跑起来费用就失控。DeepSeek 在输入侧的价格优势,正好喂饱了这种消耗模式。
另外,如果追求推理深度,还可以换deepseek-reasoner。但这个模型响应时间明显更久,在 agent 的循环里会让体验变得很拖沓。我的建议是:日常编码任务统一走deepseek-chat,只有在做复杂架构设计、代码评审这种需要深入思考的环节才临时切到deepseek-reasoner。
2.2 本地部署与云端 API 的取舍
很多团队一听到“AI coding agent”就问能不能本地部署,理由是代码不能出内网。DeepSeek 的开源权重确实给了这个选项:用 Ollama 一条命令就能拉起本地模型。
ollama run deepseek-r1:14b但我要泼一盆冷水。本地部署一个中等参数量的蒸馏版模型,和官方 API 的deepseek-chat差距不只是“小一圈”,而是在工具调用、指令跟随、代码生成的稳定性上都有肉眼可见的下降。尤其在 agent 场景里,模型一旦在某个环节理解错了工具参数,整个循环就废了,这种错误排查起来比在线 API 的问题更痛苦。
更现实的问题是:本地部署的模型上下文一旦拉长,推理速度会急剧下降。一个 14B 的模型在普通消费级显卡上处理 30K 以上上下文,一个请求等几分钟很正常,而 coding agent 可能要连续几十个请求,根本没法用。
所以我的建议是折中方案:纯离线、高敏感场景,本地模型只负责做代码摘要、短文本解释、关键词提取这类轻量任务;真正的 agent 主循环走官方 API。如果你团队的数据合规要求完全禁止任何出网,先别急着上 agent,找一台带大显存的机器跑 vLLM 部署满血版,不然体验会让你怀疑人生。
2.3 一次完整会话的成本估算
选型时团队里最关心的就是钱。我按实际项目跑出来的数据,给大家算一笔账。
假设一个中等规模的 coding agent 会话:需要 20 次请求完成一个“改 A 模块并补充测试”的任务。每次请求平均输入 50K token(因为要带上项目结构说明、相关文件内容和历史消息),输出 5K token。
一轮会话的输入总量是 50K × 20 = 1000K token,输出总量是 5K × 20 = 100K token。按 DeepSeek 当时的计价(输入约 1 元/百万 token、输出约 2 元/百万 token,命中缓存还更便宜),单次会话的成本大概是:
- 输入:1000 / 1000 × 1 = 1 元
- 输出:100 / 1000 × 2 = 0.2 元
也就是说,一个完整的、带工具调用的 agent 任务,成本约 1.2 元人民币。换算成每天跑 50 个任务,一个月支出也就是一千多块——对比人工改代码的时间成本,这个投入非常划算。当然,价格会随官方政策变化,具体以官网实时价格表为准。
3. 核心实操:从零跑通 DeepSeek coding agent
3.1 准备 API 密钥与基础环境
第一步没什么捷径:去 DeepSeek 开放平台注册账号,创建一个 API Key。创建完把密钥放到环境变量里,不要硬编码在代码中,更不要提交到 Git 仓库。
export DEEPSEEK_API_KEY="sk-xxxxxxxxxx"然后安装 OpenAI 的 Python SDK,因为 DeepSeek API 兼容 OpenAI 格式,直接用这个库最省事。
pip install openai整个过程不到五分钟。但有几个小细节需要提醒:
- 官方 base_url 是
https://api.deepseek.com,有的文档会写https://api.deepseek.com/v1,两个在大多数情况下都能用。如果遇到 404,优先试带/v1的那个。 - 环境变量名不要和 OpenAI 的
OPENAI_API_KEY混用,代码里分开读取。 - 在多人协作的团队项目里,建议用
.env文件配合python-dotenv加载密钥,而不是手动 export,避免 shell 历史泄露。
3.2 第一个代码生成请求:参数背后的细节
写一个最简单的调用,让 DeepSeek 生成一段代码:
from openai import OpenAI client = OpenAI( api_key="sk-xxxxxxxxxx", base_url="https://api.deepseek.com" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是一名资深Python工程师,只输出可直接运行的代码,不要解释。"}, {"role": "user", "content": "实现一个快速排序函数"}, ], temperature=0.3, stream=False ) print(resp.choices[0].message.content)这里面的参数值得展开讲讲,因为我在调优过程中栽过跟头。
temperature是代码生成场景里最容易被忽略的参数。很多人默认用 1.0,结果模型输出质量极不稳定,一会儿是正经代码一会儿是散文。代码任务要的是确定性和一致性,我实测下来0.2~0.4是比较好的区间;如果做代码解释、重构建议这类需要一点发散性的任务,可以放宽到0.6。高于0.8就不要用在 coding agent 里了,你会看到模型一本正经地“发明”一个不存在的 API。
stream参数在开发调试阶段建议设成False。非流式响应的错误信息更直观,方便排查;等确认逻辑没问题,再切到True提升交互体验。流式模式下记得要做超时控制,后面我会专门讲。
max_tokens也是一个必须显式设置的参数。默认值可能不够长代码生成。我给代码生成任务设置的通常是4096,先让模型把完整代码吐出来再截断,比跑一半被截断要好处理得多。
3.3 函数调用:让 Agent 真正“动起来”
这是整个项目最核心的部分。没有函数调用,你只是在“用 API 聊天”;有了函数调用,模型才能读文件、执行命令,真正变成 agent。
我先定义一个最简单的工具:读取项目文件。
tools = [ { "type": "function", "function": { "name": "read_file", "description": "读取项目中的文件内容,用于理解代码逻辑", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "文件的相对路径"}, "start_line": {"type": "integer", "description": "起始行号,可选"}, "end_line": {"type": "integer", "description": "结束行号,可选"} }, "required": ["path"] } } } ]然后写一个完整的 agent 循环。这个循环的逻辑是这样的:
- 把历史消息和工具定义发给模型。
- 如果模型返回了
tool_calls,说明它要调用工具。 - 你根据其函数名和参数执行对应函数。
- 把执行结果以
role: "tool"的消息返回给模型。 - 再让模型继续推理,直到它不再请求调用工具,才结束循环。
import json messages = [ {"role": "system", "content": "你是代码修改助手。需要先阅读相关文件,再给出修改方案。"}, {"role": "user", "content": "请阅读 src/utils.py,然后告诉我这个文件里哪个函数没有类型注解。"} ] def run_tool(name, arguments): if name == "read_file": path = arguments.get("path") with open(path, "r", encoding="utf-8") as f: return {"content": f.read()[:4000]} # 只返回前4000字符,防止撑爆上下文 return {"error": "unknown tool"} while True: resp = client.chat.completions.create( model="deepseek-chat", messages=messages, tools=tools, ) msg = resp.choices[0].message if not msg.tool_calls: print("最终答复:", msg.content) break messages.append(msg) for tc in msg.tool_calls: result = run_tool(tc.function.name, json.loads(tc.function.arguments)) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False) })这个循环里有几个关键点。
第一个是messages.append(msg)。很多人会在这一步犯错:只把工具结果加进消息列表,却忘记把模型的那条tool_calls助手消息也加进去。实际上这两条消息是成对出现的,缺失任何一边,模型的上下文就不完整,下一次请求就可能重复调用同样的工具。
第二个是tool_call_id必须和助手消息里的 ID 一一对应。API 校验很严格,如果工具结果携带的tool_call_id在历史消息里找不到,请求会直接报错。这个错误在下一章会详细讲。
第三个是工具结果要做截断。我见过有人老老实实把整个文件内容返回给模型,结果几个文件下来上下文就满了。给工具结果设置输出上限,是最有效的省 token 手段。上面代码里只取前 4000 字符就是干这个用的。
3.4 生态集成:Codex CLI 与 VSCode 插件接入
如果你不想从零写循环,可以直接把 DeepSeek 接到现成的 agent 工具里。最近社区里“Codex 接入 DeepSeek”热度很高,其实做法很简单,就是用 Codex CLI 的自定义模型配置文件。
在~/.codex/config.toml里加入:
model = "deepseek-chat" model_provider = "deepseek" [model_providers.deepseek] name = "DeepSeek" base_url = "https://api.deepseek.com/v1" env_key = "DEEPSEEK_API_KEY" wire_api = "chat"保存之后,Codex CLI 就以 DeepSeek 为后端模型开始工作。它的 agent 能力是现成的——文件读写、命令执行、模糊搜索,DeepSeek 只需要负责“决策”这一层,也就是输出工具调用指令。这就是所谓“原生生态集成”的爽点:你不需要自己维护 agent 的骨架,把精力全部放在模型调优上。
VSCode 侧的接入更简单。Cline、Continue 这类插件都支持 OpenAI 兼容提供方,在设置里填:
- Base URL:
https://api.deepseek.com - Model:
deepseek-chat - API Key:从环境变量读取
接入后,你在编辑器里选中代码、让 AI 生成改动,背后跑的就是 DeepSeek。
这里有一个很实际的教训:如果你的请求一直 401,检查 base_url 末尾是否需要加/v1;如果 404,检查模型名是否写成了deepseek-v3这种过时写法,官方当前的名字是deepseek-chat和deepseek-reasoner。
4. 多智能体协作与开发规范
4.1 多智能体架构:拆单体的正确姿势
单 agent 最大的问题是“既要又要”:又要理解需求、又要写代码、又要检查错误,一个上下文窗口根本装不下。我在项目里把 agent 拆成了三个角色。
- Planner:接收原始需求,拆解成可执行的任务列表,输出 JSON。
- Editor:按任务列表修改文件,每次只改一个模块。
- Reviewer:检查改动 diff,标记问题并给出修改建议。
它们之间的协作不搞花活,就走消息队列:Planner 的输出作为 Editor 的输入,Editor 的输出原样丢给 Reviewer。每个 agent 的上下文都是独立的,只有一份共享的项目结构清单。这个设计之所以管用,是因为把“大而全”的问题拆成了三个“小而专”的子问题,每个子问题对上下文的要求都变低了,模型出错的概率也跟着降低。
有人可能会问:为什么不把三个角色合成一个完整的 system prompt 让一个 agent 干到底?我试过,效果不太行。当模型在同一段上下文里又要规划又要写代码又要自查时,它很容易陷入自我矛盾——写完代码发现之前规划有问题,就回头改规划,改完又把代码忘了。拆开反而干净。
4.2 让 Agent 不出乱子的开发规范
多智能体系统里,最重要的事不是让模型更“聪明”,而是让它的输出可预测、可校验。我整理了几条在团队里验证过有效的规范:
- 固定 system prompt 模板。每个 agent 的 system prompt 必须包含角色定义、输出边界、禁用行为,比如“禁止删除未授权文件”“所有修改必须输出 diff”。这比在用户输入里加一大段约束稳定得多。
- 用 JSON Mode 做结构化输出。Planner 的产物不要让它自由发挥成散文,而是要求输出固定 JSON 结构,程序直接解析,不依赖提示词碰运气。
- 强制人工审核闸门。Editor 的改动不能直接合入,必须生成 diff 文件,由 Reviewer agent 先审查,再让人工确认一次。
- 给足“抄作业”的样例。在 system prompt 里附 2~3 个“输入→正确输出”的例子,比写一百字规则都管用,模型是 few-shot 学习者,给它模板它就跟着模板走。
我还留了个小技巧:在 prompt 里要求 agent 在调用工具之前,先输出一句不超过 20 字的“意图说明”。这句话不会影响工具调用,但会在日志里形成一条可追踪的操作轨迹。排查“它为什么改了这个文件”时,这句话能省下大量翻日志的时间。
5. 实战排障:高频问题与排查记录
5.1 “tool calls need immediate results”的成因与解法
这是我在项目调试阶段卡得最久的一个报错,社区里对应的搜索词热度也很高。报错信息大意是:messages tool calls need immediate results。
这个错误的成因,是你在消息列表里保留了模型生成的一条包含tool_calls的助手消息,但在这个助手消息之后,没有任何role: "tool"的消息来承接那些tool_call_id。API 的校验逻辑认为“你既然让模型调用了工具,就必须在同一轮会话里把工具结果喂回来,否则这个状态不合法”。
最常见触发场景是:你保存了上一次会话的消息历史,下一次继续使用的时候,消息列表最后一条还是assistant且带着tool_calls,但工具结果因为某种原因没写进去(比如 agent 异常退出、工具执行超时)。
解决办法也很直接,按优先级走:
- 工具结果丢失时,从消息历史里把整条
tool_calls助手消息和对应的工具结果一起裁剪掉,不要让“悬空”的工具调用留在消息里。 - 如果工具执行确实很慢,不要跳过返回,先回填一个状态型结果,例如
{"status": "running", "message": "命令仍在执行中"},让模型的工具调用有承接,但要在下一个循环里再次查询真实状态。 - 在发起请求前做一次校验,确保所有
tool_call_id都有关联的 tool 消息。简单写一个集合对比就行:
tool_ids_in_history = {m["tool_call_id"] for m in messages if m.get("role") == "tool"} for m in messages: if m.get("tool_calls"): for tc in m["tool_calls"]: if tc["id"] not in tool_ids_in_history: print("缺少工具结果:", tc["id"])排查这个报错时我还发现一个规律:DeepSeek API 的校验比某些模型后端更严格,反而帮你提前暴露了消息构造的 bug。修好之后,再切到其他模型后端也很稳定,算是一段值得的经历。
5.2 上下文膨胀与 Token 管理
coding agent 跑久了,最大的隐形杀手是上下文膨胀。每调用一次工具,工具结果都要拼进消息列表;每改一次文件,旧版本和新版本并存。跑到第十几轮,64K 的窗口就见底了,模型开始“忘记”最开始的需求。
我的处理方法分三层:
第一层,工具结果压缩。不要让工具返回完整文件内容,让 agent 自己决定读取范围,读回来之后再用一个summarize工具压缩成要点。一次读 4000 字符,压缩成 300 字符的摘要,成本立刻低一个量级。
第二层,滑动窗口裁剪。始终保持消息列表里最多保留最近 20 条消息(系统消息和需求消息除外),更早的消息直接丢弃或替换成摘要。这个方法粗糙但有效,适合大多数任务。
第三层,按需重建上下文。把“项目全局信息”和“当前任务信息”分开。全局信息只在任务开始时注入一次,任务执行中产生的过程性内容,不追加到全局上下文里。这要求 agent 的任务拆解做得够细,每个任务都相对独立,上下文就能循环复用。
还有个小细节,在messages里,系统消息要放在第一位,并且不要在循环中重复追加系统消息。我见过有人每轮都往 messages 里塞一条新的 system 消息,上下文很快就被重复内容撑爆了。
5.3 超时抖动与重试策略
DeepSeek API 的稳定性整体不错,但任何在线服务都会有抖动。coding agent 是长循环请求,一次请求超时整个任务就可能中断,所以超时处理和重试策略是必须提前做好的。
我的做法是:用 Python 写一个带指数退避的重试函数,覆盖所有chat.completions.create调用。
import time def request_with_retry(func, max_retries=3, base_delay=1.0): for attempt in range(max_retries): try: return func() except Exception as e: if attempt == max_retries - 1: raise delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5) time.sleep(delay)超时时间设置,非流式请求我一般设timeout=60,流式请求设timeout=120。流式模式下还要额外监控 chunk 间隔:如果超过 30 秒没有新 chunk 到达,主动断开重试,不要傻等。
重试时最怕的是“同一个请求被处理了两遍”。如果工具执行有副作用(比如修改文件),重试前要确保任务具备幂等性——最简单的方式是给每个请求带一个唯一任务 ID,后端做去重;做不到就去掉“重试时重新提交整整任务上下文”的做法,只重发最后一次请求。
另外,**限流(429)**也要处理。DeepSeek 对短时间密集请求会限流,指数退避里已经包含等待逻辑,但如果你有多个 agent 并行跑,建议再加一个简单的信号量,控制并发请求数在个位数,不要一股脑全发。
5.4 一个容易被忽略的稳定性细节
最后补一个不常见但值得说的坑:空响应。个别情况下,API 会返回choices列表为空或content为None的响应,尤其是在流式中断后强制切换非流式时容易出现。代码里要做兜底判断,别一拿到响应就取choices[0].message.content,先判断列表非空。
写代码时也不要有侥幸心理,所有调用返回后先校验再走下一步:
resp = client.chat.completions.create(...) if not resp.choices or not resp.choices[0].message: raise ValueError("模型返回空响应,需要重试")这套处理做完之后,我们项目的 agent 任务成功率从最初的七八成干到了九成以上,剩下的失败点基本都集中在模型能力边界而不是工程稳定性。
结尾
这次改造跑下来,我最大的感受是:DeepSeek 做 coding agent 底座,不是“能不能用”的问题,而是“怎么把工程细节做扎实”的问题。模型层面的函数调用、上下文长度都已经给你铺好了路,真正的差距在于你如何设计工具、裁剪上下文、处理异常。最后再分享一个小技巧:不要一上来就追求全自动多智能体,先用单 agent 配两三个工具跑通一个真实任务,把稳定性打磨好,再逐步加角色、加工具。一次性堆太多功能,出了问题你都分不清是模型笨还是工程错。等单循环稳定了,再上多智能体,你会回来感谢你自己的。