
最近我动手做了一个 Agent 小项目玩法不复杂模型用 DeepSeek后端跑在电脑的命令行控制台手机浏览器作为聊天入口。真正动手后才发现把“Agent 循环”“工具调用”“双端交互”串起来这件事比单纯调 API 有意思得多也踩了不少坑。这篇文章会把整条路径完整拆开为什么这么设计、Agent 的最小工作循环怎么写、DeepSeek API 怎么接、控制台和手机网页怎么共用同一套能力以及排查问题的实录。无论你是刚接触 Agent 开发还是已经会调 API 但没做过完整服务都能直接照着抄。1. 项目思路与整体设计1.1 三个关键词拼出的完整画面先给这个项目定个位控制台 Agent 指的是跑在终端里、可以连续对话并且会调用工具的智能体程序手机网页聊天则是给它套一个 Web 界面让手机浏览器通过局域网直接参与对话。两者不是两个独立系统而是同一个 Agent 后台的两个入口。控制台方便开发调试手机网页方便日常使用。为什么要这么折腾因为 Agent 的核心价值在于“能干活”。如果只是聊聊天直接在网页版 DeepSeek 里输入就行没必要自己开发。但当你需要它查询天气、算一笔复杂费用、生成一份报告、操作某个内部系统或者把多轮对话结果保存下来时就必须有一层自己的代码来承接这些需求。手机网页聊天解决的是使用场景问题你不会永远坐在电脑前也不方便给每个设备都装一套命令行环境浏览器是最通用的客户端。所以真正的核心不是“聊天界面”而是背后那个能理解意图、调用工具、维护上下文的 Agent 引擎。1.2 为什么模型选 DeepSeek选 DeepSeek 当 Agent 的“大脑”主要图三点。第一是 API 兼容性好。DeepSeek 的接口风格和 OpenAI 对齐很多开源项目都能无缝接进来。这意味着你不需要为 Agent 框架写一堆定制适配代码只要把 base_url 指向 DeepSeek再配一个 Key剩余逻辑照常跑。第二是中文能力强、上下文窗口大。Agent 的很多使用场景是中文为主的数据处理或问答DeepSeek 在中文理解上的表现一直在第一梯队。同时大上下文窗口意味着多轮会话更容易处理不用频繁做截断或压缩开发阶段省很多事。第三是成本透明且可控。自己本地部署一个 70B 级别模型需要显卡和运维成本而 API 模式按量计费个人开发阶段几乎可以忽略。当然如果后续对数据隐私要求很高也可以考虑用 Ollama 之类的工具做本地推理Agent 的代码结构不用变只需要替换模型调用层。1.3 同核多端一套 Agent两个使用入口我把整个项目拆成这么几层模型层负责语言理解和生成这一层由 DeepSeek API 提供。Agent 引擎层维护对话历史、判断是否需要调用工具、执行工具并回填结果。工具层一组普通 Python 函数比如查询时间、计算、搜索Agent 通过函数名和参数去调用。接入层一个是命令行控制台交互一个是 FastAPI 提供的网页接口。这种“同核多端”模式最大的好处是业务逻辑只写一次前端表现随便换。今天用控制台明天换网页后天想接入企业微信机器人都只是加一个接入层的事。新手最容易犯的错是把逻辑写死在 UI 回调里结果换一个入口就要重写一遍 Agent 循环。2. Agent 核心机制拆解2.1 最小 Agent 循环模型、工具、判断Agent 和普通 Chat Completion 的最大差别就是“能动手”。所谓动手靠的不是魔法而是一套循环把用户提问和当前对话历史交给模型。模型决定直接回答还是输出一个工具调用请求。如果模型请求调用工具代码去执行对应的 Python 函数。把工具返回结果作为一条新消息送回模型。模型拿到结果后给出最终回答循环结束。这个循环听起来简单但它是 Agent 的核心骨架很多复杂架构只是它的变体。实操时要注意工具返回结果一定要以“可被模型理解”的消息格式回传而不是把它打印在控制台上就完事。2.2 工具注册表让 Agent 知道有哪些工具可用为了让模型正确选择工具需要给每个工具提供一段 JSON Schema 描述里面包含名称、功能说明、参数结构。DeepSeek 的 function calling 支持和 OpenAI 协议一致。我自己定义一个工具注册表大概长这样TOOLS [ { type: function, function: { name: get_current_time, description: 获取当前日期和时间, parameters: { type: object, properties: {}, required: [] } } }, { type: function, function: { name: calculate, description: 执行四则运算表达式例如 (12)*3, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式 } }, required: [expression] } } } ]模型返回的 tool_calls 里会带上函数名和参数 JSON你只需要做一个函数分发def dispatch(name: str, arguments: dict): if name get_current_time: return get_current_time() if name calculate: return calculate(arguments[expression]) raise ValueError(f未知工具: {name})这个分发器就是工具的“总线”往后接新工具只要在此处加一行分支。2.3 上下文管理别让 Agent 失忆多轮对话里Agent 需要记住用户前面说了什么以及自己已经调过哪些工具。最简单的做法是维护一个 messages 列表每一轮都完整传给模型。messages [ {role: system, content: 你是助手。}, {role: user, content: 今天是几号}, {role: assistant, content: None, tool_calls: [...]}, {role: tool, tool_call_id: xxx, content: 2026-05-02} ]这里最容易出错的是 role 交替顺序。工具执行完以后必须把 assistant tool_calls 消息和 tool 结果消息按顺序拼接。一旦顺序乱了模型可能会把工具结果当成用户发言导致逻辑混乱。3. 实操从零搭一个双端 Agent3.1 环境准备与项目结构我先创建一个项目目录agent-demo推荐用虚拟环境隔离依赖。mkdir agent-demo cd agent-demo python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install openai fastapi uvicorn python-dotenv项目结构保持如下agent-demo/ ├── agent.py # Agent 核心循环 ├── tools.py # 工具定义和分发 ├── console.py # 控制台入口 ├── web.py # FastAPI 网页入口 └── templates/ └── index.html # 手机聊天页面.env 文件里放 DEEPSEEK_API_KEY注意不要提交到 Git。3.2 DeepSeek API 连接与基础对话DeepSeek 提供类似 OpenAI 的 SDK 调用先写一个最基础的客户端验证连通性。import os from openai import OpenAI client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com ) resp client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: 你好}] ) print(resp.choices[0].message.content)这里两个细节值得注意。第一base_url要写完整到域名不需要加/v1DeepSeek 官方会兼容处理。第二model 先用deepseek-chat它对应通用对话能力deepseek-reasoner适合复杂推理但响应更慢开发调试阶段不建议一开始就上。3.3 Agent 引擎把工具调用循环写完整在agent.py里我写一个run_agent函数。它接收用户输入和当前 messages返回新一轮的 messages。def run_agent(messages: list) - list: resp client.chat.completions.create( modeldeepseek-chat, messagesmessages, toolsTOOLS, tool_choiceauto ) msg resp.choices[0].message if not msg.tool_calls: messages.append({ role: assistant, content: msg.content }) return messages # 有工具调用先把 assistant 消息放入上下文 messages.append({ role: assistant, content: msg.content or None, tool_calls: [ { id: tc.id, type: function, function: { name: tc.function.name, arguments: tc.function.arguments } } for tc in msg.tool_calls ] }) # 逐个执行工具并回填 for tc in msg.tool_calls: name tc.function.name args json.loads(tc.function.arguments) result dispatch(name, args) messages.append({ role: tool, tool_call_id: tc.id, content: str(result) }) # 递归继续直到模型不再请求工具 return run_agent(messages)递归要注意出口当工具执行完模型下一次返回没有 tool_calls 时会更新 messages 并返回。如果写循环版本建议设置最大迭代次数比如 5 次防止模型陷入工具调用死循环。3.4 控制台交互命令行就是最好的调试器console.py很简单启动一个 REPLmessages [{role: system, content: 你是一个通用 Agent。}] while True: user_input input(你: ) if user_input.strip() in (exit, quit): break messages.append({role: user, content: user_input}) messages run_agent(messages) print(Agent:, messages[-1][content])为什么强烈建议先做控制台因为调试工具调用最方便。你在终端里能看到完整的输入输出、异常堆栈不需要考虑 HTTP 请求和浏览器兼容问题。控制台版本跑通后再包装 Web 层心态会稳很多。3.5 手机网页聊天为同一 Agent 包一层 HTTP 壳Web 层我选择 FastAPI因为体积小和 Python 生态天然契合。web.py里做两件事提供服务端渲染的聊天页面接收聊天的 POST 请求。from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse, JSONResponse from fastapi.templating import Jinja2Templates app FastAPI() templates Jinja2Templates(directorytemplates) sessions: dict[str, list] {} app.get(/, response_classHTMLResponse) async def index(request: Request): return templates.TemplateResponse(index.html, {request: request}) app.post(/chat) async def chat(req: Request): payload await req.json() session_id payload.get(session_id, default) user_text payload.get(message, ) if session_id not in sessions: sessions[session_id] [{role: system, content: 你是一个通用 Agent。}] sessions[session_id].append({role: user, content: user_text}) sessions[session_id] run_agent(sessions[session_id]) reply sessions[session_id][-1][content] return JSONResponse({reply: reply})这里要注意 sessions 字典直接存在内存里适合个人项目如果正式部署要换成 Redis 或数据库。HTML 聊天页面不需要很多样式核心是移动端适配。!DOCTYPE html html langzh head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleAgent 聊天/title style body { font-family: sans-serif; max-width: 600px; margin: auto; padding: 16px; } #chat { height: 70vh; overflow-y: auto; border: 1px solid #ddd; border-radius: 8px; padding: 12px; margin-bottom: 8px; } .user { text-align: right; color: #1a73e8; } .agent { color: #333; } #form { display: flex; gap: 8px; } #message { flex: 1; padding: 8px; } button { padding: 8px 16px; } /style /head body h3DeepSeek Agent/h3 div idchat/div form idform input idmessage placeholder输入消息... / button typesubmit发送/button /form script const session_id Math.random().toString(36).slice(2); const chat document.getElementById(chat); function append(name, text) { const div document.createElement(div); div.className name; div.textContent text; chat.appendChild(div); chat.scrollTop chat.scrollHeight; } form.addEventListener(submit, async (e) { e.preventDefault(); const input document.getElementById(message); const text input.value.trim(); if (!text) return; append(user, text); input.value ; const res await fetch(/chat, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({ session_id, message: text }) }); const data await res.json(); append(agent, data.reply); }); /script /body /html前端代码不需要框架原生的 fetch 就够了。因为只面向手机和本地控制台没有跨域、鉴权这些复杂需求。3.6 手机访问局域网让控制台 Agent 变成随身助手这是最后一步也是最容易出问题的一步。先以局域网模式启动 Web 服务uvicorn web:app --host 0.0.0.0 --port 80000.0.0.0表示监听所有网络接口不只是 localhost。然后查看电脑当前局域网 IPv4 地址Windows 用ipconfigMac 用ifconfig。比如地址是192.168.1.100手机在同一 Wi-Fi 下访问http://192.168.1.100:8000手机浏览器打开这个地址就能看到聊天页面和电脑端控制台共享同一个 Agent 后台。注意一点不要把0.0.0.0的方式随意开放到公网否则任何人猜到 IP 都可能调用你的 Agent还有 API Key 消耗风险。4. 常见问题与排查实录4.1 模型返回的工具参数老是解析失败最常见错误是直接json.loads(tc.function.arguments)抛异常。原因通常是模型生成了带注释或特殊字符的 JSON个别情况下还会返回空字符串。我的处理是先尝试解析失败就提示模型重来或者用更宽松的函数参数校验。实际开发里我还遇到过模型一次返回多个工具调用的情况。比如用户问“现在北京时间多少顺便帮我算一下 1024*256 等于多少”模型会把两个工具调用放在同一个 assistant 消息里。此时必须循环处理所有 tool_calls而不是只取第一个。很多新手在这里漏掉导致 Agent 只执行一个工具就直接回答。4.2 手机端网页打不开排查顺序固定服务是否监听0.0.0.0不是127.0.0.1。手机和电脑是否在同一个局域网。防火墙是否放行 8000 端口。电脑本地能不能打开http://127.0.0.1:8000。Windows 防火墙大概率会拦截 Python 进程需要允许 Python 访问专用网络。还有一个隐蔽问题有些路由器开启了“AP 隔离”手机能连 Wi-Fi 但无法访问电脑此时在路由器后台关掉 AP 隔离。4.3 Agent 一直在“思考”却不调用工具你可能会发现模型输出了很长一段推理文字但没有发起工具调用。这种问题多半是系统提示词的锅。在 system 里明确写清楚规则比如当你需要获取实时信息时必须调用对应工具不许编造数据。也不要同时提供太多抽象工具。工具一多模型容易晕。先用两三个直白的工具跑通再逐步增加。工具描述尽量写清“什么时候该用”比如“当用户询问日期时间时调用”效果比“获取当前时间”好得多。4.4 多轮对话后越来越慢会话越长传给模型的 token 越多响应自然变慢。这种问题不是 Bug是必然趋势。解决办法是做一个“滑动窗口”只保留最近 N 轮对话或者把早期内容做摘要。个人项目里我习惯保留最近 10 轮完整消息超过部分丢掉。注意要连 tool 消息一起处理不能只丢 user/assistant否则上下文会缺失。4.5 多个手机同时聊天时消息串线我的 sessions 字典用 session_id 区分用户。如果前端忘记传 session_id所有请求都落到default会话里两个人聊的内容就会互相污染。解决办法是在页面加载时生成一次性 ID每次请求带上。更高级一点的做法是把会话 ID 放在 Cookie 里刷新页面也不会丢。这个项目里先用 localStorage 或内存变量足够但要注意多设备测试时别混用。5. 经验总结与下一步扩展方向5.1 我在过程中踩过的几个真坑第一个坑是 API Key 泄露。最早我为了省事把 Key 直接写在前端 JavaScript 里结果手机端虽然能用但任何打开开发者工具的人都能看到。后来改成请求转发到后端Key 只保存在服务端环境变量里前端永远接触不到。第二个坑是会话消息的 role 顺序。我一度把 tool 结果拼到了 messages 末尾却不保留 assistant 的 tool_calls 记录模型直接告诉我“我没有请求过工具”。API 的上下文协议是一环扣一环的缺失一环模型认知就断掉。第三个坑是工具执行时间和 HTTP 超时冲突。有些工具会调用第三方 API耗时长而前端 fetch 默认超时比较短。后来我在 Web 端把 fetch 超时调到 120 秒并把工具执行步骤的状态通过SSE或WebSocket推送体验才好转。这个属于进阶操作新手先保持同步调用即可。5.2 从 Demo 到可用的进化路线现在这套 Agent 还比较基础但扩展空间很大。你可以做这些改进把内存 sessions 换成 Redis支持多实例部署。接入真实工具比如天气 API、数据库查询、定时任务。在工具执行前接入权限控制防止 Agent 调用危险操作。用deepseek-reasoner作为复杂任务的模型普通任务仍用deepseek-chat。引入成熟的 Agent 框架来管理编排比如 LangChain、LlamaIndex或者直接基于当前代码自己封装一层编排器。我个人的建议是入门阶段不要一上来就套大型框架。先手写一遍最小 Agent 循环你才能真正理解框架帮你做了什么。很多“框架黑魔法”看着复杂本质不过是一个循环加一堆工具注册表。现在这个项目已经变成我的私人工具箱入口。白天在电脑前用控制台指令式对话出门在外用手机浏览器随时问一句。如果你也想做一个自己的 DeepSeek Agent建议今天就动手把最小版本跑起来先不要纠结架构跑通一次工具调用胜过看十篇原理文章。