ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用DeepSeek开发Agent:控制台+手机网页聊天实战

2026/10/1 4:42:48 拓冰建站 浏览量
用DeepSeek开发Agent:控制台+手机网页聊天实战 做Agent开发有一阵子了上周刚把一个“控制台Agent 手机网页聊天”的小项目整体跑通。思路很简单用DeepSeek的API做底层模型一边在终端里跑一个能调用工具的Agent一边把同样的Agent能力封装成手机浏览器能直接访问的聊天页面。这篇分享会把项目从零到一的实现过程拆开来讲代码可以直接抄思路也可以直接复用。如果你刚接触Agent开发会Python但不知道工具调用怎么写或者正发愁怎么把大模型能力变成手机上能玩的东西那这篇文章正好对症。我踩过的坑、改过的代码、跑起来之后的细节都会原样摆出来。不吹不黑只讲实际操作。1. 项目拆解控制台Agent 手机网页聊天到底在做什么1.1 为什么选DeepSeek做Agent入门现在能接的大模型API不少但我个人觉得DeepSeek是目前做Agent入门最省心的选择原因很实在。第一它的API格式兼容OpenAI协议。这意味着你不需要学一套全新的SDK直接用常见的openaiPython库改一下base_url和api_key就能跑起来。对我这种喜欢“少折腾、多干活”的人来说这点太重要了。第二deepseek-chat这个模型本身就支持Function Calling也就是工具调用。这是Agent最核心的能力没有它Agent就只是个普通聊天机器人。入门阶段你不需要去研究RAG、微调那些复杂方向把Function Calling吃透就能做出一个能“动手干活”的Agent。第三中文理解能力强成本也低。做控制台和网页聊天模型要频繁调用如果每次调用都要花大价钱你根本不敢放开手测试。DeepSeek的价格在同类模型里属于很亲民的那一档。第四它是一个大语言模型提供的API能力不依赖本地显卡也不要求你必须有一张N卡。只要服务器能发出HTTP请求就能调用。对新手来说开发环境和工作环境是分离的你本地只需要一个Python环境就够了。1.2 Agent的工作机制它和普通聊天机器人差在哪很多人以为Agent就是“高级点的聊天机器人”这个理解不准确。我打个比方普通聊天机器人像一个只能说话的客服而Agent像一个实习生——“你说一句话他不光听还会自己去查资料、算数据、跑工具最后把结果汇报给你”。这个“自己动手”的过程在技术上靠的是模型的工具调用能力。整个流程大概是这样的用户输入一个问题比如“帮我计算一下今天到现在一共过了多少秒”。模型收到问题后先判断这个问题需要调用工具于是它返回一个特殊的响应里面包含工具名称和参数比如调用calculator参数是秒数表达式。你的程序收到这个响应后在本地执行对应的工具函数拿到结果。程序把工具结果作为一条新消息再发回给模型。模型看到工具结果后组织自然语言回答用户。整个过程是一个循环直到模型不再请求调用工具为止。这就是Agent和普通聊天机器人最本质的区别——模型不直接输出最终答案而是通过“意图判断 - 工具执行 - 结果回填”的闭环完成真正的任务。1.3 整体架构与目录设计这个项目我分成了两条链路控制台Agent和手机网页聊天。两条链路共用底层的DeepSeek API区别只在于交互层的实现方式。控制台Agent用Python脚本在终端里运行用户输入文本Agent决定要不要调用工具最终把结果打印出来。手机网页聊天用FastAPI做后端把Agent逻辑封装成一个HTTP接口前端写一个移动端适配的HTML页面手机浏览器访问后就能和Agent对话。整体目录结构不需要复杂我是这样组织的deepseek-agent-demo/ ├── console_agent.py # 控制台Agent主程序 ├── web_server.py # FastAPI后端服务 ├── templates/ │ └── index.html # 手机网页聊天页面 ├── tools.py # 工具函数定义统一放这里 └── requirements.txt # 项目依赖之所以把工具函数单独抽出来是因为控制台版本和网页版本都要用到。后续如果你想加更多工具只需要改一个文件两条链路同步生效。2. 开发前准备API申请、环境搭建、第一次调用2.1 申请DeepSeek API Key这一步很简单打开DeepSeek开放平台的官网注册账号后进入控制台在API Key管理页面创建一个新的Key。创建的时候它会要求你复制保存因为关闭页面后就不再显示完整内容了这点要注意。拿到Key后我的习惯是把它写进环境变量而不是直接硬编码在代码里。因为代码后续可能会分享、上传一旦泄露Key别人就能拿你的额度去调用。你可以这样设置export DEEPSEEK_API_KEYsk-xxxxxxxxxxxxxxxxWindows下用set命令macOS/Linux用export。设置完可以在Python里通过os.getenv(DEEPSEEK_API_KEY)读取后面所有代码都用这个方式。2.2 搭建Python虚拟环境与安装依赖我强烈建议每个小项目单独建一个虚拟环境不要一股脑全装到系统Python里。项目依赖冲突这种事早晚会找上你。mkdir deepseek-agent-demo cd deepseek-agent-demo python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # macOS/Linux激活虚拟环境后安装依赖。这个项目只需要两个核心库pip install openai fastapi uvicornopenai库用来调用DeepSeek APIfastapi和uvicorn用来跑网页后端。绝对路径下只需要这三个前端页面直接返回HTML字符串就行不需要模板引擎也不需要数据库。2.3 第一次调用DeepSeek聊天接口环境搭好之后别急着写Agent先写一个最小调用脚本确认Key和网络都正常。这个步骤能帮你把“基础环境问题”和“逻辑问题”分开排查否则后面出了问题你都不知道怪谁。import os from openai import OpenAI client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com ) response client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: 你是一个测试助手。}, {role: user, content: 请回复连接成功} ] ) print(response.choices[0].message.content)跑起来如果打印出“连接成功”说明API链路是通的。这里有几个细节说一下base_url就是https://api.deepseek.com不需要加/v1官方也支持带/v1的写法但干干净净用根路径就够了。模型名目前是deepseek-chat对应V3模型。如果你看到很多文章提到deepseek-reasoner那是推理模型侧重复杂推理API参数和返回格式略有不同。入门阶段用deepseek-chat最省事。如果出现401或403先检查Key对不对如果出现404检查base_url和模型名通常问题在这两处。3. 实现控制台Agent让模型学会“动手干活”3.1 工具调用的基本原理第一次接触Function Calling最容易懵的地方是到底是怎么个“调用”法其实模型并不会直接执行你的Python函数它只是返回一个结构化的指令。你的代码需要自己解析这个指令然后去执行对应的函数。这个关系可以理解成你雇了一个远程员工他不会用你的电脑他只是告诉你“该做第X步了”真正操作电脑的还是你自己。当你调用API时把工具的定义列表传给模型response client.chat.completions.create( modeldeepseek-chat, messagesmessages, toolstools, # 工具定义列表 tool_choiceauto # 让模型自己决定是否调用工具 )如果模型觉得该调用工具返回的response.choices[0].message.tool_calls就有内容。这个tool_calls是个列表每个元素包含function.name工具的名字比如calculatorfunction.arguments调用参数的JSON字符串比如{expression: 12 * 3 5}你的代码拿到这些信息后在本地执行再把结果构造一条roletool的消息发回给模型。模型会基于这条工具消息继续组织回答或者继续发起下一次工具调用。3.2 定义两个实用工具函数为了让Demo简单且有代表性我带大家定义两个工具一个查询当前时间一个做数学计算。这两个工具涉及了Function Calling的两种参数情况——无参数和有参数套路摸清后加任何工具都一脉相通。先建一个tools.py文件import json from datetime import datetime def get_current_time(): 无参数工具返回当前日期时间 return datetime.now().strftime(%Y-%m-%d %H:%M:%S) def safe_calculator(expression): 有参数工具执行基本数学运算 allowed set(0123456789-*/(). ) if not all(c in allowed for c in expression): return 表达式包含非法字符 try: result eval(expression) return str(result) except ZeroDivisionError: return 除数不能为0 except Exception as e: return f计算错误: {str(e)}注意计算器那个函数我用白名单过滤了一下字符只允许数字和基本运算符。因为就算是在本地跑你也不希望直接拿用户输入去eval一段任意代码这是职业习惯问题。对应的工具定义是TOOLS [ { type: function, function: { name: get_current_time, description: 获取当前系统日期和时间返回格式为 YYYY-MM-DD HH:MM:SS, parameters: { type: object, properties: {}, required: [] } } }, { type: function, function: { name: calculator, description: 执行基本数学运算支持加、减、乘、除。传入数学表达式例如 12 * 3 5, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式 } }, required: [expression] } } } ]这里有个小坑要提醒工具的description要写清楚特别是参数说明模型是靠这些描述来判断“什么时候该调用什么工具”的。描述写得越模糊模型就越容易乱来。3.3 写一个统一的分发函数在写主循环之前先写一个call_tool函数作用是接收函数名和参数字符串分发到具体的工具函数上。这相当于一个路由器把模型的“指令”翻译成真实的函数调用。def call_tool(name, arguments): if name get_current_time: return get_current_time() elif name calculator: args json.loads(arguments) return safe_calculator(args.get(expression, )) return f未知工具: {name}我习惯把json.loads放在这里统一处理这样主循环里就不用关心参数解析细节了。如果工具参数不是合法的JSONjson.loads会抛异常你也可以根据实际情况捕获。3.4 写控制台交互主循环现在进入正题写console_agent.py。整个主循环的骨架是初始化消息列表第一条是system消息告诉模型它的身份。读取用户输入把用户消息追加到消息列表。循环调用API。如果返回结果里有tool_calls就逐个执行工具把结果追加到消息列表继续循环。如果返回结果里没有tool_calls说明模型已经给出了最终回答打印并跳出内层循环等待用户下一次输入。代码如下import os from openai import OpenAI from tools import TOOLS, call_tool client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com ) def run_agent(): messages [ { role: system, content: 你是一个乐于助人的智能体你可以查询时间、做数学计算也可以正常聊天。需要工具时就用工具不需要就直接回答。 } ] print(控制台 Agent 已启动输入 exit 或 quit 退出) while True: user_input input(\n你: ).strip() if not user_input: continue if user_input.lower() in (exit, quit): print(Agent: 再见) break messages.append({role: user, content: user_input}) while True: response client.chat.completions.create( modeldeepseek-chat, messagesmessages, toolsTOOLS, tool_choiceauto ) assistant_message response.choices[0].message if assistant_message.tool_calls: messages.append({ role: assistant, content: assistant_message.content, tool_calls: [ { id: tc.id, type: function, function: { name: tc.function.name, arguments: tc.function.arguments } } for tc in assistant_message.tool_calls ] }) for tool_call in assistant_message.tool_calls: tool_name tool_call.function.name tool_args tool_call.function.arguments result call_tool(tool_name, tool_args) print(f [调用工具] {tool_name}({tool_args}) - {result}) messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) else: print(fAgent: {assistant_message.content}) break if __name__ __main__: run_agent()这里有一个特别容易踩的坑追加assistant消息的时候必须把tool_calls原样带回去。有些时候你会偷懒只记content不记工具调用信息那第二轮调用时模型就会丢失之前的上下文输出混乱。我用了一个临时列表推导式把tool_calls中的对象转换成字典是为了确保在openai SDK的不同版本下传递消息格式都能正确兼容。如果你用的是比较新的SDK版本直接这么写最稳妥。跑起来之后你可以试试这些提问你: 现在几点了 你: 帮我算一下 (156 * 7 12) / 3 等于多少 你: 杭州明天会下雨吗第三个问题模型通常不会调用工具因为我没有定义天气查询工具它就会直接回答“无法获取实时天气”。这就是Agent的正常表现——只做它能做的超出能力范围就明说不会硬编。3.5 控制台Agent的运行效果与验证运行效果大概是这样的控制台 Agent 已启动输入 exit 或 quit 退出 你: 现在几点了 [调用工具] get_current_time({}) - 2025-01-12 14:35:22 Agent: 现在是2025年1月12日14点35分22秒。 你: 帮我算一下 12 * 13 7 [调用工具] calculator({expression: 12 * 13 7}) - 163 Agent: 12乘以13等于156再加7等于163。通过打印[调用工具]那行日志你能很直观地看到Agent内部是怎么一步步做判断的这对于理解整个机制非常有帮助。4. 实现手机网页聊天从控制台到浏览器4.1 用FastAPI封装Agent服务控制台版本跑通之后下一步就是把它变成一个HTTP服务让手机浏览器能访问。我用FastAPI来实现原因很简单路由写起来痛快自动生成接口文档运行也不需要额外配置。核心逻辑把控制台里的多轮对话改成无状态的HTTP接口。因为HTTP请求本身不记住状态所以我把聊天记录存在一个Python dict里key是session_idvalue是消息列表。手机端每次发消息时带上session_id后端从dict里找到历史记录继续。import os import uuid from fastapi import FastAPI from pydantic import BaseModel from openai import OpenAI from tools import TOOLS, call_tool app FastAPI() client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com ) # 用内存dict保存每个会话的消息历史 sessions {} SYSTEM_PROMPT 你是一个乐于助人的智能体你可以查询时间、做数学计算也可以正常聊天。需要工具时就用工具不需要就直接回答。 class ChatRequest(BaseModel): message: str session_id: str app.post(/api/chat) async def chat(req: ChatRequest): sid req.session_id if not sid: sid uuid.uuid4().hex if sid not in sessions: sessions[sid] [{role: system, content: SYSTEM_PROMPT}] messages sessions[sid] messages.append({role: user, content: req.message}) while True: response client.chat.completions.create( modeldeepseek-chat, messagesmessages, toolsTOOLS, tool_choiceauto ) assistant_message response.choices[0].message if assistant_message.tool_calls: messages.append({ role: assistant, content: assistant_message.content, tool_calls: [ { id: tc.id, type: function, function: { name: tc.function.name, arguments: tc.function.arguments } } for tc in assistant_message.tool_calls ] }) for tool_call in assistant_message.tool_calls: result call_tool(tool_call.function.name, tool_call.function.arguments) messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) else: sessions[sid] messages return {session_id: sid, reply: assistant_message.content, status: ok}这里有几个实现细节说明一下session_id由前端维护每次请求带上。第一次请求时后端生成一个新的session_id并返回给前端前端存在localStorage里。消息列表是全局dict这个方案只适合开发和Demo。实际生产环境下消息列表应该存Redis或数据库不然服务重启数据就没了内存太大也会挂。我没有在这里做“上下文长度上限”限制。真正的项目里你应该在消息列表超过一定长度时做截断比如只保留最近20条或者把早期消息概括成摘要不然每次请求传的token会越来越多成本也水涨船高。4.2 写一个移动端友好的聊天页面页面我用一个纯HTML文件搞定特点是视口适配手机、页面底部固定输入框、消息气泡样式。不用前端框架因为一个单文件足够也不需要引入CDN。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno titleDeepSeek Agent 聊天/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, PingFang SC, Microsoft YaHei, sans-serif; background: #f5f5f7; display: flex; flex-direction: column; height: 100vh; } header { background: #4a6cf7; color: #fff; text-align: center; padding: 14px; font-size: 16px; font-weight: 600; } #chat-box { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; -webkit-overflow-scrolling: touch; } .msg { max-width: 78%; padding: 10px 14px; border-radius: 16px; line-height: 1.5; font-size: 15px; word-break: break-word; } .user { align-self: flex-end; background: #4a6cf7; color: #fff; border-bottom-right-radius: 4px; } .agent { align-self: flex-start; background: #fff; color: #1d1d1f; border: 1px solid #e5e5e5; border-bottom-left-radius: 4px; } .input-area { display: flex; gap: 8px; padding: 10px 12px; background: #fff; border-top: 1px solid #e5e5e5; } #input { flex: 1; border: 1px solid #ddd; border-radius: 20px; padding: 10px 14px; font-size: 16px; outline: none; } #send-btn { background: #4a6cf7; color: #fff; border: none; border-radius: 20px; padding: 10px 18px; font-size: 15px; cursor: pointer; } #send-btn:disabled { background: #a0b0f0; } /style /head body headerDeepSeek Agent/header div idchat-box/div div classinput-area input typetext idinput placeholder输入消息回车发送 button idsend-btn发送/button /div script const chatBox document.getElementById(chat-box); const inputBox document.getElementById(input); const sendBtn document.getElementById(send-btn); let sessionId localStorage.getItem(ws_session_id) || ; function addMessage(text, who) { const div document.createElement(div); div.className msg who; div.textContent text; chatBox.appendChild(div); chatBox.scrollTop chatBox.scrollHeight; } async function send() { const message inputBox.value.trim(); if (!message) return; addMessage(message, user); inputBox.value ; sendBtn.disabled true; try { const res await fetch(/api/chat, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({message: message, session_id: sessionId}) }); const data await res.json(); if (data.session_id) { sessionId data.session_id; localStorage.setItem(ws_session_id, sessionId); } addMessage(data.reply, agent); } catch (e) { addMessage(请求失败请检查网络或后端服务, agent); } finally { sendBtn.disabled false; inputBox.focus(); } } sendBtn.addEventListener(click, send); inputBox.addEventListener(keydown, function(e) { if (e.key Enter) { e.preventDefault(); send(); } }); /script /body /html几个要点maximum-scale1.0, user-scalableno是为了防止在手机上输入时页面自动缩放但注意这也是个可访问性取舍真发布到生产环境可以去掉。输入框的font-size要设成16px以上这是iOS Safari的老规矩——如果输入框字号小于16px聚焦时会自动放大页面体验很糟。localStorage保存session_id刷新页面后还能继续之前的对话不用重新开一个会话。4.3 让手机访问到本地服务页面写好之后需要把它交给FastAPI来托管。我直接在web_server.py里加一个根路由读取HTML文件内容返回即可from fastapi.responses import HTMLResponse app.get(/, response_classHTMLResponse) async def index(): with open(templates/index.html, r, encodingutf-8) as f: return HTMLResponse(f.read())然后启动服务。这里要注意要让手机能访问host必须设置为0.0.0.0这样它才会监听所有网络接口而不只是本机回环地址。uvicorn web_server:app --host 0.0.0.0 --port 8000接下来查一下你电脑在局域网里的IP。Windows下用ipconfigmacOS/Linux下用ifconfig或ip addr找那种192.168.x.x或10.x.x.x的地址。手机和电脑连同一个WiFi浏览器打开http://192.168.x.x:8000就能看到聊天页面了。整个流程不需要部署到云服务器非常适合本地开发调试。如果你在真机上打不开最常见的原因是电脑防火墙拦截了8000端口。Windows系统会弹出防火墙提醒点“允许访问”就行。macOS如果开了应用防火墙需要在系统设置里放行Python或具体端口。另外我建议直接开浏览器开发者工具切换成手机模拟模式来测试页面不用每次改代码都掏手机刷新。但最终还是要在真机上测一遍特别是输入法和键盘弹起时的体验模拟器是模拟不出真机手感的。5. 常见问题、排查技巧与后续扩展5.1 踩坑记录与排查清单我实际开发过程中遇到过不少问题整理成了一张速查表直接对应解决思路现象可能原因解决办法请求返回401/403API Key无效或账户余额不足检查DEEPSEEK_API_KEY环境变量是否设置正确去平台确认余额请求返回404base_url或模型名错误确认base_url为https://api.deepseek.com模型名正确返回结果里一直没有tool_calls模型没被要求启用工具或工具描述不清检查请求参数里是否传了tools和tool_choice工具描述尽量写具体第二次调用报Invalid message格式assistant消息里的tool_calls格式不对用第3.4节里展示的完整结构原样回传tool_calls网页打开白屏HTML文件路径不对或服务没起对端口确认templates/index.html存在路径和启动指令一致手机访问不了电脑可以防火墙拦截或绑定的是127.0.0.1启动时使用--host 0.0.0.0检查防火墙放行端口多轮对话越来越慢/费用越来越高消息列表无限累积上下文太长实现消息截断只保留最近N条或者用摘要机制压缩早期内容还有一个容易忽略的点DeepSeek支持流式输出如果你希望网页聊天像ChatGPT那样一个字一个字蹦出来需要把接口改成流式响应SSE格式。这个入门项目里我用的是非流式等响应全部生成完再一次性返回虽然简单但大模型生成耗时三五秒页面会一直转圈体验上打折。后续优化建议优先做这个。5.2 安全性和稳定性不能忽略因为是入门项目很多细节可以简化但安全意识要一开始就建立。第一API Key绝不能提交到Git仓库。我见过太多人把Key写进代码然后传到GitHub几小时就被脚本扫描盗刷。正确做法是环境变量或者本地.env文件并且把.env加进.gitignore。第二eval计算器那个工具我在前面已经做了字符白名单过滤这是在本地执行的函数可以防住大部分简单注入。但如果你要接更复杂的工具比如执行Shell命令或访问网络一定要加严密的权限校验否则Agent就是一台任人摆布的机器。第三内存会话在Demo里没问题但生产环境必须换掉。你可以用Redis存消息历史设置过期时间比如30分钟没有会话动作就自动清理这样不占内存还能天然防堆积。5.3 后续还能怎么扩展这个项目做完你其实已经掌握了Agent开发最核心的骨架模型 工具定义 对话循环。再往深走有几个方向我非常推荐。一是加更多工具。比如天气查询、汇率换算、URL内容抓取、数据库查询。每加一个工具只需要在tools.py里复制一个函数的模式再在TOOLS列表里补一条定义即可。你可以让Agent帮你查天气、查论文、查快递它就会像一个小助手一样工作。二是做多Agent协作。把单个Agent拆成“规划Agent”和“执行Agent”规划Agent负责拆解任务执行Agent负责具体调用工具。这个玩法能明显扩大Agent的任务处理范围也是目前Agent工程化的热门方向。三是接流式输出和语音输入。把HTTP接口改成SSE流式前端用EventSource或fetch的ReadableStream接收数据就可以实现打字机效果。手机上再接入语音识别Agent就变成能听懂话、能说出来回答的语音助手。四是引入框架。等你手写过一遍底层逻辑再去看LangChain、Dify这类框架你对“框架抽象了什么、自己实现到什么程度”会有完全不同的理解。先手工理解再上框架学习效率高得多。最后说一点我个人的体会。很多人学Agent是冲着“自动完成任务”去的但真正动手搭完这个项目你会发现Agent的核心难点从来不是调API而是把工具定义得足够清晰、把场景边界约束得足够明确。模型本身很聪明但它在“该不该调用工具、调用哪个工具”这件事上完全依赖你写的工具描述。你工具写得越准确Agent干活就越靠谱工具定义一模糊Agent就开始胡来。这个从“写一段调模型的代码”到“把模型能力工程化、产品化”的认知转变是这个项目里最值钱的东西。