[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"post-llm-api-compare":48,"footer-friends":100,"related-llm-api-compare":112,"nav-llm-api-compare":293,"comments-7":305,"ad-article_top":308,"ad-article_bottom":310},{"alipayQr":4,"beianNumber":5,"commentsRequireApproval":6,"contactEmail":7,"copyrightNotice":5,"donationEnabled":6,"emailCodeExpireMinutes":8,"enableRegistration":6,"followBilibiliUrl":9,"followGithubUrl":10,"followQqGroup":11,"followQqQr":12,"followWechatQr":13,"footerHtml":5,"gaTrackingId":5,"heroEnabled":6,"heroSearchPlaceholder":5,"heroSubtitle":14,"heroTitle":15,"homeCategoryTabsCount":16,"homePageSize":17,"homeProjectsCount":18,"homeSpecialsCount":19,"homeToolsCount":18,"icpNumber":20,"inviteDefaultQuota":21,"inviteRequired":22,"logoUrl":23,"mailPasswordChangedBody":24,"mailPasswordChangedSubject":25,"mailRegisterCodeBody":26,"mailRegisterCodeSubject":27,"mailResetCodeBody":28,"mailResetCodeSubject":29,"mailWelcomeBody":30,"mailWelcomeSubject":31,"miniappAppid":32,"miniappEnabled":6,"profileAvatar":33,"profileBio":34,"profileName":35,"profileWechatQr":36,"promoteEnabled":6,"promoteNotice":5,"promoteWechat":5,"requireEmailVerify":6,"sectionArticleEnabled":6,"sectionArticleTitle":37,"sectionProjectsEnabled":6,"sectionProjectsTitle":38,"sectionSpecialEnabled":6,"sectionSpecialTitle":39,"sectionToolsEnabled":6,"sectionToolsTitle":40,"seoAuthor":41,"seoBaiduTongjiId":5,"seoBaiduVerify":5,"seoCanonicalEnabled":6,"seoDescription":42,"seoGoogleVerify":5,"seoKeywords":5,"seoOgImage":23,"seoRobots":5,"seoSitemapEnabled":6,"seoTitle":43,"seoTitleTemplate":44,"showHeroStats":6,"showSidebarDonations":6,"showSidebarFeedback":6,"showSidebarFollowUs":6,"showSidebarProfile":6,"showSidebarTagCloud":6,"siteDescription":42,"siteFavicon":45,"siteName":41,"siteUrl":46,"wechatQr":47},"\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790628031878385634.png","","true","enghin110@qq.com","10","https:\u002F\u002Fspace.bilibili.com\u002F470616503","https:\u002F\u002Fgithub.com\u002Fitwuge","606262780","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790627647190853446.png","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790627638856258082.jpg","精选 AI 教程、开源项目与实用工具，助你从入门到精通","探索人工智能的无限可能","6","7","4","3","赣ICP备2023014108号-2","5","false","\u002Fimages\u002Flogo\u002Flogo.png","\u003C!DOCTYPE html>\n\u003Chtml lang=\"zh-CN\">\n\u003Chead>\u003Cmeta charset=\"UTF-8\">\u003Cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\u003C\u002Fhead>\n\u003Cbody style=\"margin:0;padding:24px 0;background:#f1f5f9;font-family:'Microsoft YaHei','PingFang SC',Helvetica,Arial,sans-serif;\">\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f1f5f9;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"padding:0 16px;\">\n        \u003Ctable role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:600px;width:100%;\">\n          \u003C!-- 品牌头部 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#3b82f6;border-radius:12px 12px 0 0;padding:20px 28px;\">\n              \u003Cspan style=\"font-size:18px;font-weight:bold;color:#ffffff;\">\u003Ca href=\"{site_url}\" style=\"color:#ffffff;text-decoration:none;\">{site_name}\u003C\u002Fa>\u003C\u002Fspan>\n              \u003Cspan style=\"float:right;font-size:12px;color:#dbeafe;line-height:26px;\">{site_description}\u003C\u002Fspan>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 正文卡片 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#ffffff;padding:32px 28px;border-radius:0 0 12px 12px;box-shadow:0 2px 8px rgba(15,23,42,0.06);\">\n              \n  \u003Ch2 style=\"margin:0 0 16px;font-size:20px;color:#1e293b;\">🔒 账户密码修改成功\u003C\u002Fh2>\n  \u003Cp style=\"margin:0 0 16px;font-size:14px;line-height:1.8;color:#475569;\">\n    你好，\u003Cstrong style=\"color:#1e293b;\">{username}\u003C\u002Fstrong>，你的账户密码已于刚刚修改成功。\n  \u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f8fafc;border-radius:10px;margin:8px 0 4px;\">\n    \u003Ctr>\u003Ctd style=\"padding:14px 18px;font-size:13px;line-height:2;color:#475569;\">\n      账户用户名：\u003Cstrong style=\"color:#1e293b;\">{username}\u003C\u002Fstrong>\u003Cbr>\n      账户邮箱：\u003Cstrong style=\"color:#1e293b;\">{email}\u003C\u002Fstrong>\u003Cbr>\n      修改时间：\u003Cstrong style=\"color:#1e293b;\">{change_time}\u003C\u002Fstrong>\n    \u003C\u002Ftd>\u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:16px 0 0;font-size:13px;line-height:1.8;color:#64748b;\">\n    如果这是你本人的操作，请忽略本邮件；\u003Cstrong style=\"color:#dc2626;\">如果不是你本人操作，说明账户密码可能已泄露\u003C\u002Fstrong>，\n    请立即使用「忘记密码」功能重置密码，并通过页脚联系邮箱告知站长。\n  \u003C\u002Fp>\n  \n  \u003Cp style=\"text-align:center;margin:28px 0 8px;\">\n    \u003Ca href=\"{site_url}\" style=\"display:inline-block;padding:12px 36px;background:#3b82f6;color:#ffffff;font-size:15px;font-weight:bold;text-decoration:none;border-radius:8px;\">进入{site_name}\u003C\u002Fa>\n  \u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 页脚 -->\n          \u003Ctr>\n            \u003Ctd align=\"center\" style=\"padding:20px 16px 0;\">\n              \n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">联系邮箱：\u003Ca href=\"mailto:{contact_email}\" style=\"color:#64748b;text-decoration:none;\">{contact_email}\u003C\u002Fa>\u003C\u002Fp>\n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">此邮件由系统自动发送，请勿直接回复 · © {year} {site_name}\u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n        \u003C\u002Ftable>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>","【{site_name}】你的账户密码已修改","\u003C!DOCTYPE html>\n\u003Chtml lang=\"zh-CN\">\n\u003Chead>\u003Cmeta charset=\"UTF-8\">\u003Cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\u003C\u002Fhead>\n\u003Cbody style=\"margin:0;padding:24px 0;background:#f1f5f9;font-family:'Microsoft YaHei','PingFang SC',Helvetica,Arial,sans-serif;\">\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f1f5f9;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"padding:0 16px;\">\n        \u003Ctable role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:600px;width:100%;\">\n          \u003C!-- 品牌头部 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#3b82f6;border-radius:12px 12px 0 0;padding:20px 28px;\">\n              \u003Cspan style=\"font-size:18px;font-weight:bold;color:#ffffff;\">\u003Ca href=\"{site_url}\" style=\"color:#ffffff;text-decoration:none;\">{site_name}\u003C\u002Fa>\u003C\u002Fspan>\n              \u003Cspan style=\"float:right;font-size:12px;color:#dbeafe;line-height:26px;\">{site_description}\u003C\u002Fspan>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 正文卡片 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#ffffff;padding:32px 28px;border-radius:0 0 12px 12px;box-shadow:0 2px 8px rgba(15,23,42,0.06);\">\n              \n  \u003Ch2 style=\"margin:0 0 16px;font-size:20px;color:#1e293b;\">注册邮箱验证码\u003C\u002Fh2>\n  \u003Cp style=\"margin:0 0 16px;font-size:14px;line-height:1.8;color:#475569;\">欢迎注册 \u003Cstrong>{site_name}\u003C\u002Fstrong>！感谢你的加入，请使用下方验证码完成邮箱验证：\u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 4px;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"background:#f8fafc;border:1px dashed #cbd5e1;border-radius:10px;padding:22px 0;\">\n        \u003Cspan style=\"font-size:32px;font-weight:bold;letter-spacing:10px;color:#3b82f6;font-family:Consolas,Menlo,monospace;\">{code}\u003C\u002Fspan>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:12px 0 0;font-size:13px;color:#64748b;\">\n    验证码 \u003Cstrong style=\"color:#dc2626;\">{expire} 分钟\u003C\u002Fstrong> 内有效，为保障账户安全，请勿向他人泄露。\n  \u003C\u002Fp>\n  \u003Chr style=\"border:none;border-top:1px solid #e2e8f0;margin:24px 0;\">\n  \n  \u003Cp style=\"margin:0 0 12px;font-size:14px;line-height:1.8;color:#475569;\">\n    这里是 \u003Cstrong style=\"color:#1e293b;\">{site_name}\u003C\u002Fstrong>，{site_description}\n  \u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 4px;\">\n    \u003Ctr>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#eff6ff;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">📚\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#1e40af;margin-top:4px;\">精选教程\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">从入门到实战\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#ecfdf5;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🚀\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#065f46;margin-top:4px;\">开源项目\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">精选可部署方案\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"34%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#fff7ed;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🧰\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#9a3412;margin-top:4px;\">在线工具\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">即开即用提效\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:20px 0 0;font-size:12px;line-height:1.8;color:#94a3b8;\">\n    如果这不是你本人的操作，请忽略本邮件，你的账户安全不会受到影响。\n  \u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 页脚 -->\n          \u003Ctr>\n            \u003Ctd align=\"center\" style=\"padding:20px 16px 0;\">\n              \n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">联系邮箱：\u003Ca href=\"mailto:{contact_email}\" style=\"color:#64748b;text-decoration:none;\">{contact_email}\u003C\u002Fa>\u003C\u002Fp>\n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">此邮件由系统自动发送，请勿直接回复 · © {year} {site_name}\u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n        \u003C\u002Ftable>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>","【{site_name}】注册验证码","\u003C!DOCTYPE html>\n\u003Chtml lang=\"zh-CN\">\n\u003Chead>\u003Cmeta charset=\"UTF-8\">\u003Cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\u003C\u002Fhead>\n\u003Cbody style=\"margin:0;padding:24px 0;background:#f1f5f9;font-family:'Microsoft YaHei','PingFang SC',Helvetica,Arial,sans-serif;\">\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f1f5f9;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"padding:0 16px;\">\n        \u003Ctable role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:600px;width:100%;\">\n          \u003C!-- 品牌头部 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#3b82f6;border-radius:12px 12px 0 0;padding:20px 28px;\">\n              \u003Cspan style=\"font-size:18px;font-weight:bold;color:#ffffff;\">\u003Ca href=\"{site_url}\" style=\"color:#ffffff;text-decoration:none;\">{site_name}\u003C\u002Fa>\u003C\u002Fspan>\n              \u003Cspan style=\"float:right;font-size:12px;color:#dbeafe;line-height:26px;\">{site_description}\u003C\u002Fspan>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 正文卡片 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#ffffff;padding:32px 28px;border-radius:0 0 12px 12px;box-shadow:0 2px 8px rgba(15,23,42,0.06);\">\n              \n  \u003Ch2 style=\"margin:0 0 16px;font-size:20px;color:#1e293b;\">找回密码验证码\u003C\u002Fh2>\n  \u003Cp style=\"margin:0 0 16px;font-size:14px;line-height:1.8;color:#475569;\">你正在找回账户密码，请使用下方验证码完成重置操作：\u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 4px;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"background:#f8fafc;border:1px dashed #cbd5e1;border-radius:10px;padding:22px 0;\">\n        \u003Cspan style=\"font-size:32px;font-weight:bold;letter-spacing:10px;color:#3b82f6;font-family:Consolas,Menlo,monospace;\">{code}\u003C\u002Fspan>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:12px 0 0;font-size:13px;color:#64748b;\">\n    验证码 \u003Cstrong style=\"color:#dc2626;\">{expire} 分钟\u003C\u002Fstrong> 内有效，为保障账户安全，请勿向他人泄露。\n  \u003C\u002Fp>\n  \u003Chr style=\"border:none;border-top:1px solid #e2e8f0;margin:24px 0;\">\n  \n  \u003Cp style=\"margin:0 0 12px;font-size:14px;line-height:1.8;color:#475569;\">\n    这里是 \u003Cstrong style=\"color:#1e293b;\">{site_name}\u003C\u002Fstrong>，{site_description}\n  \u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 4px;\">\n    \u003Ctr>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#eff6ff;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">📚\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#1e40af;margin-top:4px;\">精选教程\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">从入门到实战\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#ecfdf5;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🚀\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#065f46;margin-top:4px;\">开源项目\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">精选可部署方案\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"34%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#fff7ed;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🧰\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#9a3412;margin-top:4px;\">在线工具\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">即开即用提效\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:20px 0 0;font-size:12px;line-height:1.8;color:#94a3b8;\">\n    如果这不是你本人的操作，请忽略本邮件，你的账户安全不会受到影响。\n  \u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 页脚 -->\n          \u003Ctr>\n            \u003Ctd align=\"center\" style=\"padding:20px 16px 0;\">\n              \n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">联系邮箱：\u003Ca href=\"mailto:{contact_email}\" style=\"color:#64748b;text-decoration:none;\">{contact_email}\u003C\u002Fa>\u003C\u002Fp>\n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">此邮件由系统自动发送，请勿直接回复 · © {year} {site_name}\u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n        \u003C\u002Ftable>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>","【{site_name}】找回密码验证码","\u003C!DOCTYPE html>\n\u003Chtml lang=\"zh-CN\">\n\u003Chead>\u003Cmeta charset=\"UTF-8\">\u003Cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\u003C\u002Fhead>\n\u003Cbody style=\"margin:0;padding:24px 0;background:#f1f5f9;font-family:'Microsoft YaHei','PingFang SC',Helvetica,Arial,sans-serif;\">\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f1f5f9;\">\n    \u003Ctr>\n      \u003Ctd align=\"center\" style=\"padding:0 16px;\">\n        \u003Ctable role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:600px;width:100%;\">\n          \u003C!-- 品牌头部 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#3b82f6;border-radius:12px 12px 0 0;padding:20px 28px;\">\n              \u003Cspan style=\"font-size:18px;font-weight:bold;color:#ffffff;\">\u003Ca href=\"{site_url}\" style=\"color:#ffffff;text-decoration:none;\">{site_name}\u003C\u002Fa>\u003C\u002Fspan>\n              \u003Cspan style=\"float:right;font-size:12px;color:#dbeafe;line-height:26px;\">{site_description}\u003C\u002Fspan>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 正文卡片 -->\n          \u003Ctr>\n            \u003Ctd style=\"background:#ffffff;padding:32px 28px;border-radius:0 0 12px 12px;box-shadow:0 2px 8px rgba(15,23,42,0.06);\">\n              \n  \u003Ch2 style=\"margin:0 0 4px;font-size:20px;color:#1e293b;\">🎉 欢迎加入 {site_name}！\u003Cspan style=\"display:inline-block;margin-left:8px;padding:2px 8px;background:#dcfce7;color:#15803d;font-size:12px;border-radius:10px;vertical-align:middle;\">邮箱已验证\u003C\u002Fspan>\u003C\u002Fh2>\n  \u003Cp style=\"margin:0 0 16px;font-size:14px;line-height:1.8;color:#475569;\">\n    你好，\u003Cstrong style=\"color:#1e293b;\">{username}\u003C\u002Fstrong>，你的账户已注册成功。\n  \u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f8fafc;border-radius:10px;margin:8px 0 4px;\">\n    \u003Ctr>\u003Ctd style=\"padding:14px 18px;font-size:13px;line-height:2;color:#475569;\">\n      账户用户名：\u003Cstrong style=\"color:#1e293b;\">{username}\u003C\u002Fstrong>\u003Cbr>\n      注册邮箱：\u003Cstrong style=\"color:#1e293b;\">{email}\u003C\u002Fstrong>\u003Cbr>\n      注册奖励：\u003Cstrong style=\"color:#dc2626;\">+{register_points} 积分\u003C\u002Fstrong>已发放\n    \u003C\u002Ftd>\u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Chr style=\"border:none;border-top:1px solid #e2e8f0;margin:24px 0;\">\n  \n  \u003Cp style=\"margin:0 0 12px;font-size:14px;line-height:1.8;color:#475569;\">\n    这里是 \u003Cstrong style=\"color:#1e293b;\">{site_name}\u003C\u002Fstrong>，{site_description}\n  \u003C\u002Fp>\n  \u003Ctable role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0 4px;\">\n    \u003Ctr>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#eff6ff;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">📚\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#1e40af;margin-top:4px;\">精选教程\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">从入门到实战\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"33%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#ecfdf5;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🚀\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#065f46;margin-top:4px;\">开源项目\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">精选可部署方案\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n      \u003Ctd width=\"34%\" valign=\"top\" style=\"padding:6px;\">\n        \u003Cdiv style=\"background:#fff7ed;border-radius:8px;padding:14px 10px;text-align:center;\">\n          \u003Cdiv style=\"font-size:15px;\">🧰\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:13px;font-weight:bold;color:#9a3412;margin-top:4px;\">在线工具\u003C\u002Fdiv>\n          \u003Cdiv style=\"font-size:12px;color:#64748b;margin-top:2px;\">即开即用提效\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n  \u003Cp style=\"margin:16px 0 0;font-size:14px;line-height:1.8;color:#475569;\">登录后你可以：\u003C\u002Fp>\n  \u003Cul style=\"margin:8px 0 0;padding-left:22px;font-size:14px;line-height:2;color:#475569;\">\n    \u003Cli>浏览、收藏优质内容与实用工具\u003C\u002Fli>\n    \u003Cli>投稿教程与项目，审核通过后获得积分奖励\u003C\u002Fli>\n    \u003Cli>参与评论、打分，与其他用户交流\u003C\u002Fli>\n    \u003Cli>通过签到、投稿、互动持续累积积分与等级\u003C\u002Fli>\n  \u003C\u002Ful>\n  \n  \u003Cp style=\"text-align:center;margin:28px 0 8px;\">\n    \u003Ca href=\"{site_url}\" style=\"display:inline-block;padding:12px 36px;background:#3b82f6;color:#ffffff;font-size:15px;font-weight:bold;text-decoration:none;border-radius:8px;\">进入{site_name}\u003C\u002Fa>\n  \u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n          \u003C!-- 页脚 -->\n          \u003Ctr>\n            \u003Ctd align=\"center\" style=\"padding:20px 16px 0;\">\n              \n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">联系邮箱：\u003Ca href=\"mailto:{contact_email}\" style=\"color:#64748b;text-decoration:none;\">{contact_email}\u003C\u002Fa>\u003C\u002Fp>\n              \u003Cp style=\"margin:6px 0 0;font-size:12px;color:#94a3b8;\">此邮件由系统自动发送，请勿直接回复 · © {year} {site_name}\u003C\u002Fp>\n            \u003C\u002Ftd>\n          \u003C\u002Ftr>\n        \u003C\u002Ftable>\n      \u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftable>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>","【{site_name}】欢迎加入{site_name}，账户注册成功","7111479@qq.com","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790627623420492239.jpg","全栈开发者，专注 AI 应用与效率工具","武哥","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790629453503799753.png","热门爆款","精选项目","精选专题","精选工具","AI导航工具站","精选优质 AI 教程、AI 工具与开源项目，助力开发者快速上手人工智能","AI导航工具站 - AI 教程 · AI 项目 · AI 工具","%s - AI导航工具站","\u002Fimages\u002Flogo\u002Ffavicon.ico","https:\u002F\u002Fwuge.fun","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790628026196296604.png",{"id":49,"title":50,"slug":51,"summary":52,"content":53,"coverImage":54,"categoryId":55,"authorId":55,"viewCount":56,"likeCount":56,"commentCount":56,"isTop":56,"isRecommend":56,"status":55,"rejectReason":5,"publishedAt":57,"createdAt":58,"updatedAt":59,"category":60,"tags":65,"author":92,"wordCount":98,"readingTime":99},7,"大模型 API 调用入门：OpenAI、DeepSeek、通义千问全对比","llm-api-compare","想把 AI 能力接进自己的程序？一文讲清三大主流大模型 API 的申请、价格对比、Python 调用示例和流式输出实现，附省钱技巧。","## 为什么要用 API 而不是网页版\n\n网页版 AI 是给人用的，API 是给程序用的。当你想实现这些功能时，就必须用 API：\n\n- 给自己的网站\u002FApp 加上 AI 对话功能\n- 批量处理数据（比如给 1000 篇文章自动打标签）\n- 搭建自动化工作流（收到邮件 → AI 总结 → 推送到微信）\n- 开发自己的 AI 应用\n\n好消息是：**主流大模型厂商都兼容 OpenAI 的接口格式**，学会一个等于学会全部。\n\n## 三大平台申请与价格对比\n\n| 平台 | 申请地址 | 代表模型 | 价格（输入\u002F输出，每百万 token） | 特点 |\n|------|---------|---------|------------------------------|------|\n| DeepSeek | platform.deepseek.com | deepseek-chat | ¥2 \u002F ¥8（缓存命中仅 ¥0.5） | 性价比之王，推理能力强 |\n| 通义千问 | bailian.console.aliyun.com | qwen-plus | ¥0.8 \u002F ¥2（qwen-turbo 更便宜） | 阿里生态，新用户送额度 |\n| OpenAI | platform.openai.com | gpt-4o-mini | $0.15 \u002F $0.6 | 生态最成熟，需海外支付 |\n\n> 价格为公开刊例价，各平台经常调价和促销，以官网为准。日常测试用 DeepSeek 或 qwen-turbo，一天几分钱。\n\n**token 怎么理解**：1 个中文字约等于 1-2 个 token。一次普通问答（问 100 字 + 答 500 字）大约消耗 1000 token，成本不到 1 分钱。\n\n## 通用调用模板（三平台共用）\n\n因为都兼容 OpenAI 格式，所以代码几乎一样，只需改三个东西：`api_key`、`base_url`、`model`。\n\n```python\nfrom openai import OpenAI\n\n# —— 配置区：三选一，取消对应注释即可 ——\n\n# DeepSeek\nclient = OpenAI(api_key=\"sk-xxx\", base_url=\"https:\u002F\u002Fapi.deepseek.com\")\nMODEL = \"deepseek-chat\"\n\n# 通义千问（阿里云百炼）\n# client = OpenAI(api_key=\"sk-xxx\",\n#                 base_url=\"https:\u002F\u002Fdashscope.aliyuncs.com\u002Fcompatible-mode\u002Fv1\")\n# MODEL = \"qwen-plus\"\n\n# OpenAI\n# client = OpenAI(api_key=\"sk-xxx\")  # 默认 base_url 即官方地址\n# MODEL = \"gpt-4o-mini\"\n\n# —— 调用区：完全相同的代码 ——\nresp = client.chat.completions.create(\n    model=MODEL,\n    messages=[\n        {\"role\": \"system\", \"content\": \"你是一位简洁专业的技术助手\"},\n        {\"role\": \"user\", \"content\": \"用一句话解释什么是 API\"},\n    ],\n    temperature=0.7,\n)\nprint(resp.choices[0].message.content)\n```\n\n## 必须理解的 4 个概念\n\n**1. messages 的角色体系**\n\n- `system`：给 AI 定人设和规则，优先级最高，用户看不到\n- `user`：用户说的话\n- `assistant`：AI 之前的回复（多轮对话时要把历史一起传）\n\n**API 是无状态的**——它不记得你上次说过什么。多轮对话需要你自己把历史消息全部带上：\n\n```python\nhistory = [{\"role\": \"system\", \"content\": \"你是翻译助手\"}]\n\nwhile True:\n    user_input = input(\"你：\")\n    history.append({\"role\": \"user\", \"content\": user_input})\n    resp = client.chat.completions.create(model=MODEL, messages=history)\n    reply = resp.choices[0].message.content\n    history.append({\"role\": \"assistant\", \"content\": reply})  # 关键：存下 AI 的回复\n    print(\"AI：\", reply)\n```\n\n**2. temperature（创造性）**\n\n0~2 之间，越低越保守确定，越高越发散。写代码、翻译用 0~0.3；头脑风暴、写文案用 0.7~1.0。\n\n**3. max_tokens（输出长度上限）**\n\n不设的话默认拉满，可能产生意料外的费用。生产环境建议显式设置。\n\n**4. 流式输出（Streaming）**\n\n网页版 AI「一个字一个字蹦出来」的效果就是流式输出。长回答时体验天差地别：\n\n```python\nstream = client.chat.completions.create(\n    model=MODEL,\n    messages=[{\"role\": \"user\", \"content\": \"写一篇 500 字的短文，介绍成都\"}],\n    stream=True,  # 开启流式\n)\nfor chunk in stream:\n    delta = chunk.choices[0].delta.content\n    if delta:\n        print(delta, end=\"\", flush=True)  # 逐块打印，模拟打字机\n```\n\n## 实战：给 100 篇文章自动打标签\n\n学了就要用，这个例子把 API 用进真实工作：\n\n```python\nimport json\nimport time\nfrom openai import OpenAI\n\nclient = OpenAI(api_key=\"sk-xxx\", base_url=\"https:\u002F\u002Fapi.deepseek.com\")\n\ndef tag_article(title: str, content: str) -> dict:\n    \"\"\"调用大模型为文章打标签，返回结构化结果\"\"\"\n    resp = client.chat.completions.create(\n        model=\"deepseek-chat\",\n        messages=[{\n            \"role\": \"user\",\n            \"content\": f\"\"\"为下面的文章分类并打标签。\n\n标题：{title}\n正文：{content[:2000]}\n\n要求：\n1. category 从 [AI教程, 机器学习, 深度学习, 自然语言处理, 计算机视觉] 中选一个\n2. tags 提取 3-5 个关键词\n3. 严格输出 JSON，不要输出任何其他内容\n\n格式：{{\"category\": \"...\", \"tags\": [\"...\", \"...\"]}}\"\"\",\n        }],\n        temperature=0,  # 分类任务要确定性\n    )\n    return json.loads(resp.choices[0].message.content)\n\n# 批量处理\narticles = [\n    {\"title\": \"卷积神经网络入门\", \"content\": \"CNN 是一种……\"},\n    # ... 更多文章\n]\nfor art in articles:\n    result = tag_article(art[\"title\"], art[\"content\"])\n    print(art[\"title\"], \"->\", result)\n    time.sleep(0.5)  # 控制频率，避免触发限流\n```\n\n## 省钱与稳定性技巧\n\n**1. 能短的别长**：输入输出都计费。让 AI「用 100 字以内回答」，提示词里别贴无关内容。\n\n**2. 善用缓存**：DeepSeek 的上下文缓存命中后输入价打 1 折。把不变的系统提示词放前面，变动的内容放后面，命中率更高。\n\n**3. 加重试和降级**：API 会抖动，生产代码必须容错：\n\n```python\nfrom tenacity import retry, stop_after_attempt, wait_exponential\n\n@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=2, max=10))\ndef chat_with_retry(messages):\n    return client.chat.completions.create(model=MODEL, messages=messages)\n```\n\n**4. 密钥别硬编码**：用环境变量 `os.environ[\"DEEPSEEK_API_KEY\"]`，别把 key 提交进 Git——GitHub 上有专门的扫描器盯着泄露的 key，几小时内就会被盗刷。\n\n**5. 监控用量**：各平台控制台都有用量统计，建议设消费上限告警，防止程序 bug 导致刷爆余额。\n\n## 下一步\n\n掌握了基础调用，可以继续探索：\n\n- **Function Calling**：让 AI 调用你的函数（查天气、查数据库），这是做 Agent 的基础\n- **结构化输出**：强制 AI 返回符合 JSON Schema 的数据\n- **批量接口**：OpenAI 的 Batch API 价格打 5 折，适合离线大批量任务\n\n先用本文的代码跑通第一个请求——看到自己程序里蹦出 AI 的回复，那种感觉和用网页版完全不同。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790635573979484250.png",1,0,"2026-09-29T06:46:34.779822889+08:00","2026-09-29T06:46:13.979605838+08:00","2026-09-29T06:46:34.779879782+08:00",{"id":55,"name":61,"slug":62,"description":5,"icon":63,"sortOrder":55,"postCount":56,"parentId":56,"createdAt":64,"updatedAt":64},"AI 教程","ai-tutorials","i-ri-book-open-line","2026-09-29T04:25:29.669355545+08:00",[66,72,77,82,87],{"id":67,"name":68,"slug":69,"color":70,"postCount":56,"createdAt":71},6,"Python","python","#e2e8f0","2026-09-29T04:25:30.347030306+08:00",{"id":73,"name":74,"slug":75,"color":70,"postCount":56,"createdAt":76},74,"OpenAI","openai","2026-09-29T04:25:30.853760595+08:00",{"id":78,"name":79,"slug":80,"color":70,"postCount":56,"createdAt":81},89,"DeepSeek","deepseek","2026-09-29T06:46:13.756746391+08:00",{"id":83,"name":84,"slug":85,"color":70,"postCount":56,"createdAt":86},104,"API","api","2026-09-29T06:46:13.964167454+08:00",{"id":88,"name":89,"slug":90,"color":70,"postCount":56,"createdAt":91},105,"通义千问","tong-yi-qian-wen","2026-09-29T06:46:13.971569076+08:00",{"id":55,"username":93,"nickname":94,"avatarUrl":95,"bio":5,"rewardWechatQr":96,"rewardAlipayQr":97},"wuge","站长","\u002Fstatic\u002Fuploads\u002Favatars\u002F2026\u002F09\u002F29\u002F1790630804049538805.jpg","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790630754672518549.png","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790630762294788382.png",445,2,[101,107],{"id":99,"name":102,"url":103,"logo":104,"description":102,"sortOrder":56,"status":55,"createdAt":105,"updatedAt":106},"哔哩哔哩","https:\u002F\u002Fwww.bilibili.com\u002F","\\images\\social\\bilibili.png","0001-01-01T00:00:00Z","2026-09-29T04:44:41.867019475+08:00",{"id":55,"name":108,"url":109,"logo":110,"description":108,"sortOrder":56,"status":55,"createdAt":111,"updatedAt":111},"GitHub","https:\u002F\u002Fgithub.com","\\images\\social\\github.png","2026-09-29T04:32:39.65740234+08:00",[113,145,174,206,233,261],{"id":114,"title":115,"slug":116,"summary":117,"content":118,"coverImage":119,"categoryId":120,"authorId":55,"viewCount":56,"likeCount":56,"commentCount":56,"isTop":56,"isRecommend":56,"status":55,"rejectReason":5,"publishedAt":121,"createdAt":122,"updatedAt":123,"category":124,"tags":127},20,"用 Python 调用大模型 API：写一个命令行 AI 助手","ai-cli","毕业项目来了：用 100 行 Python 写一个命令行 AI 助手，支持流式输出、打字机效果、多轮对话记忆，最后打包成 exe 发给朋友用。","## 我们要做什么\n\n把前面几篇笔记学到的知识串起来，做一个真正能用的东西：\n\n```text\n$ python ai_chat.py\n🤖 AI 助手已启动（输入 \u002Fquit 退出，\u002Fclear 清空记忆）\n\n你 > 用一句话介绍 Python\nAI > Python 是一种简洁易读、生态丰富的通用编程语言。\n\n你 > 它适合做什么\nAI > （记得上文）Python 特别适合数据分析、AI 开发、自动化脚本……\n```\n\n功能清单：\n\n- 流式输出（打字机效果）\n- 多轮对话记忆\n- `\u002Fquit`、`\u002Fclear`、`\u002Fsave` 斜杠命令\n- 配置文件管理 API Key\n- 打包成 exe 双击即用\n\n## 第 1 步：基础对话循环\n\n```python\n# ai_chat.py\nimport os\nimport sys\n\nfrom openai import OpenAI\n\n# 从环境变量读 key，不要硬编码\nclient = OpenAI(\n    api_key=os.environ.get(\"DEEPSEEK_API_KEY\", \"sk-在这里填你的key\"),\n    base_url=\"https:\u002F\u002Fapi.deepseek.com\",\n)\nMODEL = \"deepseek-chat\"\n\n# 对话历史：system 人设 + 历轮问答\nhistory = [\n    {\"role\": \"system\", \"content\": \"你是简洁专业的 AI 助手，回答控制在 300 字以内\"}\n]\n\ndef chat(user_input: str) -> None:\n    \"\"\"发送消息并流式打印回复\"\"\"\n    history.append({\"role\": \"user\", \"content\": user_input})\n    stream = client.chat.completions.create(\n        model=MODEL, messages=history, stream=True\n    )\n    print(\"AI > \", end=\"\", flush=True)\n    full_reply = \"\"\n    for chunk in stream:\n        delta = chunk.choices[0].delta.content\n        if delta:\n            print(delta, end=\"\", flush=True)  # 逐块打印 = 打字机效果\n            full_reply += delta\n    print()\n    # 关键：把 AI 的回复存回历史，实现记忆\n    history.append({\"role\": \"assistant\", \"content\": full_reply})\n\ndef main() -> None:\n    print(\"🤖 AI 助手已启动（\u002Fquit 退出，\u002Fclear 清空记忆）\\n\")\n    while True:\n        try:\n            user_input = input(\"你 > \").strip()\n        except (KeyboardInterrupt, EOFError):  # Ctrl+C \u002F Ctrl+D 优雅退出\n            print(\"\\n再见！\")\n            break\n        if not user_input:\n            continue\n        if user_input == \"\u002Fquit\":\n            print(\"再见！\")\n            break\n        if user_input == \"\u002Fclear\":\n            history[:] = history[:1]  # 只保留 system 人设\n            print(\"（记忆已清空）\\n\")\n            continue\n        try:\n            chat(user_input)\n        except Exception as e:\n            print(f\"出错了：{e}\\n\")  # 网络错误不退出，下次还能用\n        print()\n\nif __name__ == \"__main__\":\n    main()\n```\n\n到这里已经是一个能用的 AI 助手了：流式输出、有记忆、能清空、出错不崩。\n\n## 第 2 步：控制记忆长度\n\n对话历史无限增长会撞上两个天花板：模型的上下文窗口、以及你的余额（历史每轮都重新计费）。加一个滑动窗口：\n\n```python\nMAX_HISTORY = 20  # 最多保留最近 20 条消息\n\ndef trim_history() -> None:\n    \"\"\"保留 system + 最近的对话，超出的最旧记录删掉\"\"\"\n    if len(history) > MAX_HISTORY + 1:\n        del history[1 : len(history) - MAX_HISTORY]\n```\n\n在 `chat()` 开头调用一次 `trim_history()` 即可。更精细的方案是按 token 数裁剪，但按条数对日常使用已足够。\n\n## 第 3 步：保存对话记录\n\n`\u002Fsave` 命令把当前会话存成 Markdown 文件：\n\n```python\nfrom datetime import datetime\n\ndef save_history() -> str:\n    \"\"\"导出对话到 Markdown 文件，返回文件名\"\"\"\n    lines = [f\"# AI 对话记录 {datetime.now():%Y-%m-%d %H:%M}\\n\"]\n    for msg in history[1:]:  # 跳过 system\n        role = \"**你**\" if msg[\"role\"] == \"user\" else \"**AI**\"\n        lines.append(f\"{role}：{msg['content']}\\n\")\n    filename = f\"chat_{datetime.now():%Y%m%d_%H%M%S}.md\"\n    with open(filename, \"w\", encoding=\"utf-8\") as f:\n        f.write(\"\\n\".join(lines))\n    return filename\n```\n\n在主循环里加分支：\n\n```python\n        if user_input == \"\u002Fsave\":\n            print(f\"（已保存到 {save_history()}）\\n\")\n            continue\n```\n\n## 第 4 步：配置文件管理 Key\n\nKey 写死在代码里既危险又不方便分享，改用配置文件：\n\n```python\n# config.py\nimport json\nfrom pathlib import Path\n\nCONFIG_PATH = Path.home() \u002F \".ai_chat_config.json\"\n\ndef load_config() -> dict:\n    \"\"\"读取配置，没有则引导用户创建\"\"\"\n    if not CONFIG_PATH.exists():\n        key = input(\"首次使用，请输入你的 API Key：\").strip()\n        CONFIG_PATH.write_text(\n            json.dumps({\"api_key\": key, \"model\": \"deepseek-chat\"}, ensure_ascii=False, indent=2),\n            encoding=\"utf-8\",\n        )\n        print(f\"配置已保存到 {CONFIG_PATH}\\n\")\n    return json.loads(CONFIG_PATH.read_text(encoding=\"utf-8\"))\n```\n\n主程序里：\n\n```python\nconfig = load_config()\nclient = OpenAI(api_key=config[\"api_key\"], base_url=\"https:\u002F\u002Fapi.deepseek.com\")\nMODEL = config[\"model\"]\n```\n\n配置文件放在用户主目录，既不用每次输入，也不会误提交进 Git。\n\n## 第 5 步：打包成 exe\n\n让没有 Python 环境的朋友也能用：\n\n```bash\npip install pyinstaller\npyinstaller --onefile --name ai-chat ai_chat.py\n```\n\n产物在 `dist\u002Fai-chat.exe`，双击即用，体积约 15MB（含 Python 运行时）。\n\n几个实用参数：\n\n```bash\npyinstaller --onefile --name ai-chat --clean ai_chat.py\n# --onefile  打成单个 exe\n# --clean    清掉上次构建缓存，改代码后重新打包建议加上\n```\n\n## 完整效果与扩展方向\n\n最终成品是一个 100 行左右的单文件工具。跑通之后，可以按需扩展：\n\n**加「温度」调节**：`\u002Ftemp 0.3` 命令动态调整创造性参数。\n\n**加预设角色**：`\u002Frole 翻译` 切换 system 人设，一个工具当多个助手用。\n\n**统计用量**：从响应里取 `usage` 字段，显示本次消耗了多少 token、约合多少钱：\n\n```python\n# 非流式响应可直接读 usage；流式需设置 stream_options={\"include_usage\": True}\nstream = client.chat.completions.create(\n    model=MODEL, messages=history, stream=True,\n    stream_options={\"include_usage\": True},\n)\n# 最后一个 chunk 里有 usage 信息\n```\n\n**接入本地模型**：把 `base_url` 换成 `http:\u002F\u002Flocalhost:11434\u002Fv1`（Ollama），这个工具就变成了离线版 AI 助手——代码一行都不用改，这就是 OpenAI 兼容接口的妙处。\n\n## 回顾：这个项目用到了什么\n\n| 知识点 | 出处 |\n|--------|------|\n| 虚拟环境管理依赖 | 笔记 1 |\n| 第三方库调用网络 API | 笔记 2 |\n| 异常处理与优雅退出 | 笔记 6 |\n| 配置文件与文件读写 | 笔记 6\u002F7 |\n| 字符串处理 | 笔记 9 |\n\n这就是我们 Python 专题的初衷：**学语法的最好方式是做东西**。这个命令行助手可能就是你以后每天用 AI 的方式之一——自己动手做的工具，用起来总是格外顺手。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138763089017.png",8,"2026-09-29T06:56:29.738842766+08:00","2026-09-29T06:55:38.763211716+08:00","2026-09-29T06:56:29.738903836+08:00",{"id":120,"name":125,"slug":68,"description":125,"icon":5,"sortOrder":56,"postCount":56,"parentId":56,"createdAt":126,"updatedAt":126},"Python专题","2026-09-29T06:55:05.938571812+08:00",[128,129,130,135,140],{"id":67,"name":68,"slug":69,"color":70,"postCount":56,"createdAt":71},{"id":78,"name":79,"slug":80,"color":70,"postCount":56,"createdAt":81},{"id":131,"name":132,"slug":133,"color":70,"postCount":56,"createdAt":134},150,"AI","ai","2026-09-29T06:55:38.743861502+08:00",{"id":136,"name":137,"slug":138,"color":70,"postCount":56,"createdAt":139},151,"命令行工具","ming-ling-xing-gong-ju","2026-09-29T06:55:38.751023469+08:00",{"id":141,"name":142,"slug":143,"color":70,"postCount":56,"createdAt":144},152,"项目实战","xiang-mu-shi-zhan","2026-09-29T06:55:38.757712461+08:00",{"id":146,"title":147,"slug":148,"summary":149,"content":150,"coverImage":151,"categoryId":120,"authorId":55,"viewCount":55,"likeCount":56,"commentCount":56,"isTop":56,"isRecommend":56,"status":55,"rejectReason":5,"publishedAt":152,"createdAt":153,"updatedAt":154,"category":155,"tags":156},11,"Python 虚拟环境完全指南：venv、conda、uv 怎么选","venv","为什么每个 Python 项目都需要虚拟环境？venv、conda、uv 三大方案深度对比，附常见坑排查清单，看完彻底告别依赖地狱。","## 为什么需要虚拟环境\n\n假设你同时在开发两个项目：\n\n- 项目 A 是去年写的，依赖 `Django 3.2`\n- 项目 B 是新项目，要用 `Django 5.0`\n\nPython 默认把第三方包装在**同一个全局目录**里。装了 Django 5.0，项目 A 直接跑不起来；退回 3.2，项目 B 又报错。这就是「依赖地狱」。\n\n虚拟环境的解决思路很直接：**给每个项目造一个独立的 Python 运行空间**，各装各的包，互不干扰。\n\n```text\n电脑\n├── 全局 Python 3.12\n├── 项目A\u002Fvenv\u002F   ← Django 3.2\n├── 项目B\u002Fvenv\u002F   ← Django 5.0\n└── 项目C\u002Fvenv\u002F   ← 一堆 AI 库\n```\n\n## 方案一：venv（官方内置，默认首选）\n\nPython 3.3+ 自带，零安装，适合 90% 的纯 Python 项目。\n\n**创建与激活**：\n\n```bash\n# 在项目目录下创建虚拟环境（目录名约定俗成叫 .venv 或 venv）\npython -m venv .venv\n\n# Windows 激活\n.venv\\Scripts\\activate\n\n# macOS\u002FLinux 激活\nsource .venv\u002Fbin\u002Factivate\n```\n\n激活成功的标志：命令行提示符前面出现 `(.venv)`。\n\n**日常使用**：\n\n```bash\n# 装包（装在虚拟环境里，不污染全局）\npip install requests pandas\n\n# 导出依赖清单（项目交接、部署时用）\npip freeze > requirements.txt\n\n# 别人拿到项目后还原环境\npip install -r requirements.txt\n\n# 退出虚拟环境\ndeactivate\n```\n\n**优点**：官方标准、零依赖、任何 Python 环境都有。\n\n**缺点**：只能管理 Python 包，管不了 CUDA、MySQL 客户端这类系统级依赖；不能切换 Python 版本（venv 创建时用什么版本就是什么版本）。\n\n## 方案二：conda（数据科学标配）\n\nAnaconda\u002FMiniconda 提供的环境管理器，**不仅能管 Python 包，还能管 Python 解释器本身和非 Python 依赖**。\n\n**常用命令**：\n\n```bash\n# 创建环境，同时指定 Python 版本\nconda create -n myproject python=3.11\n\n# 激活 \u002F 退出\nconda activate myproject\nconda deactivate\n\n# 装包\nconda install numpy pandas\n# conda 仓库没有的包，可以混用 pip\npip install some-package\n\n# 导出与还原\nconda env export > environment.yml\nconda env create -f environment.yml\n\n# 查看所有环境 \u002F 删除环境\nconda env list\nconda remove -n myproject --all\n```\n\n**建议装 Miniconda 而不是 Anaconda**：Anaconda 预装了几百个包，体积 3G+；Miniconda 只有核心，按需安装。\n\n**什么时候必须选 conda**：需要特定版本的 CUDA\u002FcuDNN（深度学习训练）、需要 GDAL 这类难装的地理信息库、需要在同一台机器维护多个 Python 版本。\n\n## 方案三：uv（2024 年后的新王）\n\nAstral 公司（ruff 的作者）用 Rust 写的包管理器，**速度比 pip 快 10-100 倍**，正在迅速成为新标准。\n\n```bash\n# 安装 uv（Windows PowerShell）\npowershell -c \"irm https:\u002F\u002Fastral.sh\u002Fuv\u002Finstall.ps1 | iex\"\n\n# 创建项目（自动生成 .venv、pyproject.toml）\nuv init myproject\ncd myproject\n\n# 装包（秒装，第一次用会惊到）\nuv add requests pandas\n\n# 运行脚本（自动用项目环境，不用手动激活）\nuv run main.py\n\n# 锁定依赖（生成 uv.lock，保证全团队环境一致）\nuv lock\n```\n\nuv 最大的改进是**把 venv、pip、pyproject.toml、锁定文件整合成一套连贯的工作流**，以前要四五个工具配合的事，现在一个命令搞定。\n\n## 怎么选：一张决策表\n\n| 你的情况 | 推荐 |\n|---------|------|\n| 纯 Python 项目，图省心 | venv |\n| 深度学习\u002F数据科学，要 CUDA | conda |\n| 新项目，追求速度和现代工作流 | uv |\n| 团队协作、要锁定依赖版本 | uv |\n| 老旧项目维护 | 跟着项目原来的方案走 |\n\n**新手建议**：从 venv 学起理解原理，新项目直接上 uv，碰到 CUDA 相关的再上 conda。三者概念相通，切换成本很低。\n\n## 常见坑排查清单\n\n**1. 装了包却提示 ModuleNotFoundError**\n\n90% 的原因是：包装在了 A 环境，程序却在 B 环境运行。排查：\n\n```bash\n# 看当前 python 在哪\npython -c \"import sys; print(sys.executable)\"\n\n# 看包装在了哪\npip show 包名 | findstr Location\n```\n\n两个路径对不上就是环境问题。\n\n**2. Windows 激活脚本报错「禁止运行脚本」**\n\nPowerShell 执行策略限制，管理员身份运行一次：\n\n```powershell\nSet-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\n```\n\n**3. 虚拟环境目录要不要提交 Git**\n\n**不要**。`.venv\u002F` 动辄几百 MB，应该加进 `.gitignore`，用 `requirements.txt` 或 `uv.lock` 传递依赖信息。\n\n**4. IDE 里代码飘红但命令行能跑**\n\nIDE 用的解释器和命令行不是同一个。VS Code 按 `Ctrl+Shift+P` → `Python: Select Interpreter`，选项目 `.venv` 里的那个。\n\n**5. requirements.txt 写成一团乱**\n\n手动维护比 `pip freeze` 更好——freeze 会把所有间接依赖都写进去。推荐只写直接依赖：\n\n```text\nrequests>=2.31\npandas>=2.0\n```\n\n## 写在最后\n\n虚拟环境是 Python 工程化的第一课。养成习惯：**每个新项目第一件事就是建虚拟环境**。项目越多的老司机越清楚——今天你省下的 30 秒，就是明天某次神秘报错时要还的债。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138336381447.png","2026-09-29T06:56:46.91774372+08:00","2026-09-29T06:55:38.336536895+08:00","2026-09-29T06:56:46.917805846+08:00",{"id":120,"name":125,"slug":68,"description":125,"icon":5,"sortOrder":56,"postCount":56,"parentId":56,"createdAt":126,"updatedAt":126},[157,158,163,166,170],{"id":67,"name":68,"slug":69,"color":70,"postCount":56,"createdAt":71},{"id":159,"name":160,"slug":161,"color":70,"postCount":56,"createdAt":162},118,"虚拟环境","xu-ni-huan-jing","2026-09-29T06:55:38.308255024+08:00",{"id":164,"name":148,"slug":148,"color":70,"postCount":56,"createdAt":165},119,"2026-09-29T06:55:38.316270691+08:00",{"id":167,"name":168,"slug":168,"color":70,"postCount":56,"createdAt":169},120,"conda","2026-09-29T06:55:38.323353011+08:00",{"id":171,"name":172,"slug":172,"color":70,"postCount":56,"createdAt":173},121,"uv","2026-09-29T06:55:38.330626142+08:00",{"id":175,"title":176,"slug":177,"summary":178,"content":179,"coverImage":180,"categoryId":120,"authorId":55,"viewCount":56,"likeCount":56,"commentCount":56,"isTop":56,"isRecommend":56,"status":55,"rejectReason":5,"publishedAt":181,"createdAt":182,"updatedAt":183,"category":184,"tags":185},12,"Python requests 爬虫入门：从发送请求到解析 HTML","requests-spider","Python 爬虫第一课：用 requests + BeautifulSoup 抓取网页数据。从 GET\u002FPOST 请求、请求头伪装到数据提取，附完整实战案例和反爬应对策略。","## 先立规矩：合法合规爬取\n\n写代码之前必须说清楚：\n\n- 只爬**公开数据**，不碰需要登录才能看的内容\n- 遵守网站的 `robots.txt` 协议（在域名后加 `\u002Frobots.txt` 可查看）\n- 控制频率，别把人家服务器压垮（就是讲礼貌）\n- 数据仅供个人学习，不用于商业用途，不爬个人隐私信息\n\n技术无罪，用法有责。下面的练习我们以公开的静态页面为例。\n\n## 爬虫的本质：三步走\n\n```text\n1. 发送请求（requests）→ 服务器返回 HTML 文本\n2. 解析内容（BeautifulSoup）→ 从 HTML 里提取要的数据\n3. 保存数据（csv\u002Fjson\u002F数据库）→ 落盘\n```\n\n安装依赖：\n\n```bash\npip install requests beautifulsoup4 lxml\n```\n\n## 第一步：requests 发送请求\n\n**最基本的 GET 请求**：\n\n```python\nimport requests\n\nresp = requests.get(\"https:\u002F\u002Fexample.com\")\nprint(resp.status_code)  # 200 表示成功\nprint(resp.text[:200])   # 网页 HTML 前 200 字符\n```\n\n**带参数的 GET**：\n\n```python\n# 等价于访问 https:\u002F\u002Fexample.com\u002Fsearch?q=python&page=2\nparams = {\"q\": \"python\", \"page\": 2}\nresp = requests.get(\"https:\u002F\u002Fexample.com\u002Fsearch\", params=params)\nprint(resp.url)  # 自动拼好的完整 URL\n```\n\n**POST 请求**（模拟表单提交）：\n\n```python\ndata = {\"username\": \"test\", \"password\": \"123456\"}\nresp = requests.post(\"https:\u002F\u002Fexample.com\u002Flogin\", data=data)\n```\n\n**响应对象常用属性**：\n\n```python\nresp.status_code   # 状态码：200 成功，404 不存在，403 被拒绝\nresp.text          # 文本内容（自动按推测编码解码）\nresp.content       # 二进制内容（下载图片\u002F文件用这个）\nresp.json()        # 接口返回 JSON 时直接转成字典\nresp.encoding      # 乱码时手动指定：resp.encoding = \"utf-8\"\n```\n\n## 第二步：请求头伪装（过不了反爬就看这里）\n\n很多网站会检查请求头，发现是程序访问就拒绝。加上请求头伪装成浏览器：\n\n```python\nheaders = {\n    \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) \"\n                  \"AppleWebKit\u002F537.36 (KHTML, like Gecko) \"\n                  \"Chrome\u002F120.0.0.0 Safari\u002F537.36\",\n    \"Referer\": \"https:\u002F\u002Fexample.com\u002F\",  # 有些站检查来源页\n}\nresp = requests.get(url, headers=headers, timeout=10)\n```\n\n`User-Agent` 可以直接从浏览器复制：F12 打开开发者工具 → Network → 随便点一个请求 → Headers → Request Headers。\n\n**超时设置是必须的**：不加 `timeout`，目标服务器不响应时程序会永远卡住。\n\n## 第三步：BeautifulSoup 解析 HTML\n\n拿到 HTML 后，用 CSS 选择器提取数据：\n\n```python\nfrom bs4 import BeautifulSoup\n\nhtml = \"\"\"\n\u003Chtml>\u003Cbody>\n  \u003Cdiv class=\"article\">\n    \u003Ch2>标题一\u003C\u002Fh2>\n    \u003Cp class=\"intro\">简介一\u003C\u002Fp>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"article\">\n    \u003Ch2>标题二\u003C\u002Fh2>\n    \u003Cp class=\"intro\">简介二\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Fbody>\u003C\u002Fhtml>\n\"\"\"\n\nsoup = BeautifulSoup(html, \"lxml\")\n\n# 最常用的 4 个方法\nsoup.select(\"h2\")            # CSS 选择器，返回所有匹配的列表\nsoup.select_one(\"h2\")        # 只返回第一个\nsoup.find(\"div\", class_=\"article\")   # 按标签+属性查找\nsoup.find_all(\"h2\")          # 返回所有匹配\n\n# 提取内容\nfor article in soup.select(\"div.article\"):\n    title = article.select_one(\"h2\").text.strip()\n    intro = article.select_one(\"p.intro\").text.strip()\n    print(title, \"|\", intro)\n```\n\n**提取属性**（如链接地址、图片地址）：\n\n```python\nlink = soup.select_one(\"a\")\nprint(link[\"href\"])     # 取 href 属性\nprint(link.get_text())  # 取链接文字\n```\n\n## 完整实战：抓取本站文章列表\n\n下面是一个可以直接改造的实战模板：\n\n```python\nimport csv\nimport time\n\nimport requests\nfrom bs4 import BeautifulSoup\n\nHEADERS = {\n    \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) \"\n                  \"AppleWebKit\u002F537.36 Chrome\u002F120.0.0.0 Safari\u002F537.36\"\n}\n\ndef fetch_page(url: str) -> str:\n    \"\"\"下载页面，带重试\"\"\"\n    for attempt in range(3):\n        try:\n            resp = requests.get(url, headers=HEADERS, timeout=10)\n            resp.raise_for_status()  # 4xx\u002F5xx 直接抛异常\n            resp.encoding = \"utf-8\"\n            return resp.text\n        except requests.RequestException as e:\n            print(f\"第 {attempt + 1} 次请求失败: {e}\")\n            time.sleep(2)\n    return \"\"\n\ndef parse_page(html: str) -> list[dict]:\n    \"\"\"解析页面，提取文章列表（选择器按目标网站实际结构调整）\"\"\"\n    soup = BeautifulSoup(html, \"lxml\")\n    articles = []\n    for item in soup.select(\"div.post-item\"):\n        title_tag = item.select_one(\"h2 a\")\n        if not title_tag:\n            continue\n        articles.append({\n            \"title\": title_tag.get_text(strip=True),\n            \"url\": title_tag.get(\"href\", \"\"),\n            \"date\": item.select_one(\".date\").get_text(strip=True)\n                    if item.select_one(\".date\") else \"\",\n        })\n    return articles\n\ndef save_csv(data: list[dict], filename: str = \"articles.csv\"):\n    \"\"\"保存到 CSV，Excel 可直接打开\"\"\"\n    with open(filename, \"w\", newline=\"\", encoding=\"utf-8-sig\") as f:\n        writer = csv.DictWriter(f, fieldnames=[\"title\", \"url\", \"date\"])\n        writer.writeheader()\n        writer.writerows(data)\n    print(f\"已保存 {len(data)} 条到 {filename}\")\n\nif __name__ == \"__main__\":\n    all_data = []\n    for page in range(1, 4):  # 抓前 3 页\n        url = f\"https:\u002F\u002Fexample.com\u002Fposts?page={page}\"\n        html = fetch_page(url)\n        if html:\n            all_data.extend(parse_page(html))\n        time.sleep(1.5)  # 礼貌间隔，别给人家服务器压力\n    save_csv(all_data)\n```\n\n## 动态渲染页面怎么办\n\n上面的方法只适合**服务端渲染**的页面（HTML 里直接有数据）。如果数据是 JS 动态加载的（`resp.text` 里找不到要的内容），两条路：\n\n**路线一：找接口（优先）**\n\nF12 → Network → Fetch\u002FXHR，刷新页面，找返回 JSON 的接口。很多网站前端就是调这些接口拿数据的，直接请求接口比解析 HTML 省事十倍：\n\n```python\nresp = requests.get(\"https:\u002F\u002Fexample.com\u002Fapi\u002Fposts?page=1\", headers=HEADERS)\ndata = resp.json()  # 直接拿到结构化数据，连解析都不用\n```\n\n**路线二：上浏览器自动化**\n\n接口藏得太深时，用 Playwright 驱动真实浏览器：\n\n```bash\npip install playwright && playwright install chromium\n```\n\n```python\nfrom playwright.sync_api import sync_playwright\n\nwith sync_playwright() as p:\n    browser = p.chromium.launch(headless=True)\n    page = browser.new_page()\n    page.goto(\"https:\u002F\u002Fexample.com\")\n    page.wait_for_selector(\"div.post-item\")  # 等内容渲染出来\n    html = page.content()\n    browser.close()\n# 之后照常 BeautifulSoup 解析\n```\n\n## 常见反爬与应对\n\n| 反爬手段 | 表现 | 应对 |\n|---------|------|------|\n| UA 检测 | 403 拒绝 | 加真实 User-Agent |\n| 频率限制 | 429 或封 IP | time.sleep 降速、换 IP |\n| 登录墙 | 跳转登录页 | requests.Session() 保持 Cookie |\n| 验证码 | 弹验证 | 降低频率避开触发点，不建议硬刚 |\n| 动态渲染 | HTML 里没数据 | 找接口或用 Playwright |\n\n## 写在最后\n\n爬虫 80% 的工作量是「分析目标网站」而不是写代码：数据在 HTML 里还是接口里？分页参数长什么样？有没有反爬？先花 20 分钟用 F12 把网站看透，代码往往 20 行就够了。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138387839504.png","2026-09-29T06:56:45.03460923+08:00","2026-09-29T06:55:38.387959519+08:00","2026-09-29T06:56:45.034666561+08:00",{"id":120,"name":125,"slug":68,"description":125,"icon":5,"sortOrder":56,"postCount":56,"parentId":56,"createdAt":126,"updatedAt":126},[186,187,192,196,201],{"id":67,"name":68,"slug":69,"color":70,"postCount":56,"createdAt":71},{"id":188,"name":189,"slug":190,"color":70,"postCount":56,"createdAt":191},122,"爬虫","pa-chong","2026-09-29T06:55:38.359394779+08:00",{"id":193,"name":194,"slug":194,"color":70,"postCount":56,"createdAt":195},123,"requests","2026-09-29T06:55:38.366815934+08:00",{"id":197,"name":198,"slug":199,"color":70,"postCount":56,"createdAt":200},124,"BeautifulSoup","beautifulsoup","2026-09-29T06:55:38.372320794+08:00",{"id":202,"name":203,"slug":204,"color":70,"postCount":56,"createdAt":205},125,"数据采集","shu-ju-cai-ji","2026-09-29T06:55:38.379703082+08:00",{"id":207,"title":208,"slug":209,"summary":210,"content":211,"coverImage":212,"categoryId":120,"authorId":55,"viewCount":56,"likeCount":56,"commentCount":56,"isTop":56,"isRecommend":56,"status":55,"rejectReason":5,"publishedAt":213,"createdAt":214,"updatedAt":215,"category":216,"tags":217},13,"Pandas 数据处理 10 个高频操作，一篇就够","pandas","日常数据分析 80% 的工作只用这 10 个操作：读写文件、筛选、分组聚合、缺失值处理、表合并。每个操作附可直接套用的代码模板。","## 为什么是 Pandas\n\n处理表格数据，Excel 能做的事 Pandas 都能做，而且：\n\n- **快**：几十万行数据秒级处理，Excel 早就卡死\n- **可复现**：代码写一次，下个月的新数据重新跑一遍就行\n- **可串联**：读数据 → 清洗 → 分析 → 出图一条龙\n\n```bash\npip install pandas openpyxl  # openpyxl 用于读写 Excel\n```\n\n## 准备演示数据\n\n后面的例子都用这份「销售数据」：\n\n```python\nimport pandas as pd\n\ndf = pd.DataFrame({\n    \"姓名\": [\"张三\", \"李四\", \"王五\", \"张三\", \"李四\", \"赵六\"],\n    \"部门\": [\"华东\", \"华北\", \"华东\", \"华东\", \"华北\", \"华南\"],\n    \"销售额\": [12000, 8500, 15000, 13500, None, 9000],\n    \"入职日期\": [\"2023-01-15\", \"2023-03-20\", \"2022-11-05\",\n                 \"2023-01-15\", \"2023-03-20\", \"2024-02-01\"],\n})\n```\n\n## 操作 1：读写 CSV \u002F Excel\n\n```python\n# 读取\ndf = pd.read_csv(\"sales.csv\")                      # CSV\ndf = pd.read_excel(\"sales.xlsx\", sheet_name=\"1月\")  # Excel 指定工作表\n\n# 中文乱码？指定编码\ndf = pd.read_csv(\"sales.csv\", encoding=\"gbk\")\n\n# 保存（index=False 去掉行号，utf-8-sig 保证 Excel 打开不乱码）\ndf.to_csv(\"output.csv\", index=False, encoding=\"utf-8-sig\")\ndf.to_excel(\"output.xlsx\", index=False)\n```\n\n## 操作 2：快速查看数据\n\n拿到任何数据，先做这四个动作：\n\n```python\ndf.head()        # 前 5 行长什么样\ndf.info()        # 每列的类型、有没有缺失\ndf.describe()    # 数值列的统计：均值\u002F最值\u002F分位数\ndf.shape         # 几行几列\n```\n\n## 操作 3：选列与筛选行\n\n```python\n# 选列\ndf[\"销售额\"]                    # 单列（Series）\ndf[[\"姓名\", \"销售额\"]]          # 多列（DataFrame）\n\n# 条件筛选（最常用的操作，没有之一）\ndf[df[\"销售额\"] > 10000]\ndf[df[\"部门\"] == \"华东\"]\ndf[(df[\"销售额\"] > 10000) & (df[\"部门\"] == \"华东\")]  # 多条件用 & |，必须加括号\n\n# 模糊匹配\ndf[df[\"姓名\"].str.contains(\"张\")]\n\n# 在列表内匹配\ndf[df[\"部门\"].isin([\"华东\", \"华南\"])]\n```\n\n## 操作 4：排序与去重\n\n```python\n# 排序\ndf.sort_values(\"销售额\", ascending=False)           # 按销售额降序\ndf.sort_values([\"部门\", \"销售额\"], ascending=[True, False])  # 先部门升序再销售额降序\n\n# 去重\ndf.drop_duplicates()                      # 整行重复才删\ndf.drop_duplicates(subset=[\"姓名\"])       # 同一人只留一条\ndf.drop_duplicates(subset=[\"姓名\"], keep=\"last\")  # 留最后一次出现的\n```\n\n## 操作 5：缺失值处理\n\n```python\n# 查看缺失情况\ndf.isna().sum()      # 每列有几个缺失\n\n# 处理：三选一\ndf.dropna()                              # 1. 删掉含缺失的行\ndf[\"销售额\"].fillna(0)                   # 2. 填固定值\ndf[\"销售额\"].fillna(df[\"销售额\"].mean()) # 3. 填均值（数值列常用）\n```\n\n## 操作 6：分组聚合（groupby，核心中的核心）\n\n「按部门统计销售额」这类需求的标准答案：\n\n```python\n# 按部门分组，算销售额总和\ndf.groupby(\"部门\")[\"销售额\"].sum()\n\n# 一次算多个统计量\ndf.groupby(\"部门\")[\"销售额\"].agg([\"sum\", \"mean\", \"count\"])\n\n# 多列分组 + 不同列不同聚合\ndf.groupby(\"部门\").agg(\n    总销售额=(\"销售额\", \"sum\"),\n    人均销售额=(\"销售额\", \"mean\"),\n    人数=(\"姓名\", \"count\"),\n).reset_index()  # 把分组键变回普通列，方便后续处理\n```\n\n## 操作 7：新增计算列\n\n```python\n# 直接运算\ndf[\"提成\"] = df[\"销售额\"] * 0.05\n\n# 条件赋值：销售额过万的标记为「高」\nimport numpy as np\ndf[\"等级\"] = np.where(df[\"销售额\"] > 10000, \"高\", \"普通\")\n\n# 对文本列做处理\ndf[\"姓名长度\"] = df[\"姓名\"].str.len()\n```\n\n## 操作 8：表合并（merge 与 concat）\n\n```python\n# concat：上下堆叠（合并多个月份的同结构表格）\ndf_all = pd.concat([df_1月, df_2月, df_3月], ignore_index=True)\n\n# merge：左右拼接（类似 SQL 的 JOIN）\ndf_员工 = pd.DataFrame({\"姓名\": [\"张三\", \"李四\"], \"职级\": [\"P5\", \"P6\"]})\ndf.merge(df_员工, on=\"姓名\", how=\"left\")  # 左连接：保留左表全部行\n```\n\n`how` 的四种取值：`left`（保留左表）、`right`、`inner`（交集）、`outer`（并集）。日常 90% 用 `left`。\n\n## 操作 9：日期处理\n\n```python\n# 字符串转日期\ndf[\"入职日期\"] = pd.to_datetime(df[\"入职日期\"])\n\n# 提取年\u002F月\u002F星期几\ndf[\"入职年\"] = df[\"入职日期\"].dt.year\ndf[\"入职月\"] = df[\"入职日期\"].dt.month\n\n# 按月统计\ndf.groupby(df[\"入职日期\"].dt.to_period(\"M\"))[\"销售额\"].sum()\n\n# 日期差（算工龄）\ndf[\"工龄天数\"] = (pd.Timestamp.now() - df[\"入职日期\"]).dt.days\n```\n\n## 操作 10：透视表（pivot_table）\n\nExcel 透视表的代码版，多维交叉统计一把梭：\n\n```python\n# 行=部门，列=等级，值=销售额总和\ndf.pivot_table(\n    index=\"部门\",\n    columns=\"等级\",\n    values=\"销售额\",\n    aggfunc=\"sum\",\n    fill_value=0,      # 空位填 0\n    margins=True,      # 加合计行列\n)\n```\n\n## 避坑指南\n\n**1. SettingWithCopyWarning 警告**\n\n```python\n# 错误：先筛选再赋值，改的可能不是原表\ndf[df[\"部门\"] == \"华东\"][\"销售额\"] = 0\n\n# 正确：用 loc 一步定位\ndf.loc[df[\"部门\"] == \"华东\", \"销售额\"] = 0\n```\n\n**2. 链式赋值结果没生效**\n\nPandas 很多操作**返回新对象而不改原表**。要么接返回值：\n\n```python\ndf = df.dropna()  # 接返回值\n# 或者\ndf.dropna(inplace=True)  # 用 inplace 参数\n```\n\n**3. 数字被读成字符串**\n\nCSV 里带「¥」「,」的数字会被当成文本，先清洗再计算：\n\n```python\ndf[\"销售额\"] = df[\"销售额\"].str.replace(\"¥\", \"\").str.replace(\",\", \"\").astype(float)\n```\n\n## 写在最后\n\nPandas 的学习曲线不在语法而在**思维转换**：别写 for 循环逐行处理，学会用「向量化」的批量操作。遇到需求先问自己——这能不能用筛选、分组、合并这三板斧解决？90% 的情况答案是能。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138431846763.png","2026-09-29T06:56:42.1304477+08:00","2026-09-29T06:55:38.43196821+08:00","2026-09-29T06:56:42.13052425+08:00",{"id":120,"name":125,"slug":68,"description":125,"icon":5,"sortOrder":56,"postCount":56,"parentId":56,"createdAt":126,"updatedAt":126},[218,219,223,228],{"id":67,"name":68,"slug":69,"color":70,"postCount":56,"createdAt":71},{"id":220,"name":221,"slug":209,"color":70,"postCount":56,"createdAt":222},126,"Pandas","2026-09-29T06:55:38.411798198+08:00",{"id":224,"name":225,"slug":226,"color":70,"postCount":56,"createdAt":227},127,"数据分析","shu-ju-fen-xi","2026-09-29T06:55:38.419436825+08:00",{"id":229,"name":230,"slug":231,"color":70,"postCount":56,"createdAt":232},128,"数据处理","shu-ju-chu-li","2026-09-29T06:55:38.426699835+08:00",{"id":234,"title":235,"slug":236,"summary":237,"content":238,"coverImage":239,"categoryId":120,"authorId":55,"viewCount":56,"likeCount":56,"commentCount":56,"isTop":56,"isRecommend":56,"status":55,"rejectReason":5,"publishedAt":240,"createdAt":241,"updatedAt":242,"category":243,"tags":244},14,"Python 装饰器详解：从原理到实战的 5 个经典用法","decorator","装饰器是 Python 最优雅也最劝退的语法。从闭包原理讲起，用计时、缓存、权限校验等 5 个真实场景把装饰器彻底讲透。","## 装饰器解决什么问题\n\n先看一个真实痛点。项目里有一堆函数，现在要给**每一个**都加上「记录执行时间」的功能：\n\n```python\ndef get_user_list():\n    # ... 业务逻辑\n    pass\n\ndef create_order():\n    # ... 业务逻辑\n    pass\n```\n\n笨办法：每个函数内部都加两行计时代码。20 个函数改 20 处，以后要加「记录日志」再改 20 处——烦且容易漏。\n\n装饰器的思路：**写一个「包装机」，把原函数包一层，在不改原函数代码的前提下增加新功能**。\n\n```python\n@timer   # 这一行就是装饰器，自动给函数加上计时功能\ndef get_user_list():\n    pass\n```\n\n## 原理：装饰器就是「函数套函数」\n\n理解装饰器只需要知道两件事：\n\n1. **函数可以当参数传递**，也可以当返回值返回\n2. **闭包**：内部函数可以记住外部函数的变量\n\n最简装饰器长这样：\n\n```python\nimport functools\nimport time\n\ndef timer(func):\n    \"\"\"装饰器：统计函数执行时间\"\"\"\n    @functools.wraps(func)  # 保留原函数的名字和文档，好习惯\n    def wrapper(*args, **kwargs):\n        start = time.time()\n        result = func(*args, **kwargs)   # 执行原函数\n        print(f\"{func.__name__} 耗时 {time.time() - start:.3f} 秒\")\n        return result\n    return wrapper\n\n@timer\ndef slow_function():\n    time.sleep(1)\n\nslow_function()  # 输出：slow_function 耗时 1.002 秒\n```\n\n`@timer` 这行代码等价于 `slow_function = timer(slow_function)`——把原函数传进 timer，用返回的 wrapper 替换掉原来的名字。之后每次调用 `slow_function()`，实际执行的是包装后的 wrapper。\n\n## 实战 1：日志记录\n\n```python\nimport functools\nimport logging\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(message)s\")\n\ndef log_calls(func):\n    \"\"\"记录函数调用的参数和返回值\"\"\"\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        logging.info(f\"调用 {func.__name__}，args={args}, kwargs={kwargs}\")\n        result = func(*args, **kwargs)\n        logging.info(f\"{func.__name__} 返回 {result}\")\n        return result\n    return wrapper\n\n@log_calls\ndef add(a, b):\n    return a + b\n\nadd(3, 5)\n# 输出两行日志：调用 add，args=(3, 5), kwargs={} \u002F add 返回 8\n```\n\n## 实战 2：缓存（记忆化）\n\n递归算斐波那契数列，不加缓存时 fib(35) 要算好几秒，因为重复计算了大量子问题。加个缓存装饰器：\n\n```python\nimport functools\n\ndef cache(func):\n    \"\"\"把算过的结果存起来，相同参数直接返回\"\"\"\n    saved = {}\n    @functools.wraps(func)\n    def wrapper(*args):\n        if args not in saved:\n            saved[args] = func(*args)\n        return saved[args]\n    return wrapper\n\n@cache\ndef fib(n):\n    if n \u003C 2:\n        return n\n    return fib(n - 1) + fib(n - 2)\n\nprint(fib(100))  # 秒出结果\n```\n\n**其实不用自己写**：标准库 `functools.lru_cache` 就是现成的缓存装饰器，还带容量淘汰策略：\n\n```python\nfrom functools import lru_cache\n\n@lru_cache(maxsize=128)\ndef fib(n):\n    if n \u003C 2:\n        return n\n    return fib(n - 1) + fib(n - 2)\n```\n\n## 实战 3：权限校验（Web 开发天天用）\n\n```python\nimport functools\n\n# 模拟当前登录用户\ncurrent_user = {\"name\": \"张三\", \"role\": \"admin\"}\n\ndef require_role(role):\n    \"\"\"带参数的装饰器：要求特定角色才能访问\"\"\"\n    def decorator(func):\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs):\n            if current_user.get(\"role\") != role:\n                raise PermissionError(f\"需要 {role} 权限\")\n            return func(*args, **kwargs)\n        return wrapper\n    return decorator\n\n@require_role(\"admin\")\ndef delete_all_data():\n    print(\"数据已删除\")\n\ndelete_all_data()  # 正常执行\n# 把 current_user[\"role\"] 改成 \"guest\" 再调用 → PermissionError: 需要 admin 权限\n```\n\n注意这里变成了**三层函数嵌套**：`require_role(\"admin\")` 先拿到参数返回装饰器，装饰器再接收函数。规律：**装饰器带参数，就多套一层**。\n\nFlask 里的 `@app.route(\"\u002Fapi\")`、登录校验 `@login_required`，全都是这个模式。\n\n## 实战 4：重试机制\n\n调用不稳定的网络接口时特别好用：\n\n```python\nimport functools\nimport time\n\ndef retry(times=3, delay=1):\n    \"\"\"失败自动重试\"\"\"\n    def decorator(func):\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs):\n            for attempt in range(1, times + 1):\n                try:\n                    return func(*args, **kwargs)\n                except Exception as e:\n                    print(f\"第 {attempt} 次失败：{e}\")\n                    if attempt == times:\n                        raise\n                    time.sleep(delay)\n        return wrapper\n    return decorator\n\n@retry(times=3, delay=2)\ndef call_unstable_api():\n    import random\n    if random.random() \u003C 0.7:   # 70% 概率失败，模拟不稳定接口\n        raise ConnectionError(\"网络超时\")\n    return \"成功\"\n```\n\n## 实战 5：函数注册表（进阶但极其有用）\n\n插件系统、命令分发器的标准写法：\n\n```python\nCOMMANDS = {}  # 命令注册表\n\ndef register(name):\n    \"\"\"把函数注册到命令表\"\"\"\n    def decorator(func):\n        COMMANDS[name] = func\n        return func   # 不改函数行为，只登记\n    return decorator\n\n@register(\"hello\")\ndef cmd_hello():\n    print(\"你好！\")\n\n@register(\"bye\")\ndef cmd_bye():\n    print(\"再见！\")\n\n# 根据用户输入动态调用\nuser_input = \"hello\"\nCOMMANDS[user_input]()   # 输出：你好！\n```\n\n新增命令只需要加新函数并装饰，调用处的分发逻辑一行不用改——这就是「开放封闭原则」。\n\n## 避坑指南\n\n**1. 别忘了 @functools.wraps**\n\n不加的话，被装饰函数的 `__name__`、`__doc__` 都会变成 wrapper 的，调试和文档生成时会错乱。\n\n**2. 装饰顺序很重要**\n\n```python\n@timer\n@log_calls\ndef f(): ...\n# 等价于 f = timer(log_calls(f))\n# 执行顺序：先 timer 的包装，进去后先记日志再执行，出来后计时\n```\n\n**3. 装饰器在导入时就执行**\n\n`@register` 这类装饰器在**模块导入时**就跑完了，不是调用时才跑。利用这个特性做自动注册，也要注意别在装饰器里写耗时操作拖慢启动。\n\n## 写在最后\n\n装饰器的学习路径：先会用 `@lru_cache`、`@app.route` 这些现成的 → 再读懂别人的装饰器 → 最后才会写自己的。别反过来。先把本文的 5 个例子各敲一遍，用到的场景出现时，你自然会长出「这里该用装饰器」的直觉。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138476604049.png","2026-09-29T06:56:39.89932248+08:00","2026-09-29T06:55:38.476769783+08:00","2026-09-29T06:56:39.899403489+08:00",{"id":120,"name":125,"slug":68,"description":125,"icon":5,"sortOrder":56,"postCount":56,"parentId":56,"createdAt":126,"updatedAt":126},[245,246,251,256],{"id":67,"name":68,"slug":69,"color":70,"postCount":56,"createdAt":71},{"id":247,"name":248,"slug":249,"color":70,"postCount":56,"createdAt":250},129,"装饰器","zhuang-shi-qi","2026-09-29T06:55:38.452280181+08:00",{"id":252,"name":253,"slug":254,"color":70,"postCount":56,"createdAt":255},130,"闭包","bi-bao","2026-09-29T06:55:38.460791287+08:00",{"id":257,"name":258,"slug":259,"color":70,"postCount":56,"createdAt":260},131,"进阶","jin-jie","2026-09-29T06:55:38.468837931+08:00",{"id":262,"title":263,"slug":264,"summary":265,"content":266,"coverImage":267,"categoryId":120,"authorId":55,"viewCount":56,"likeCount":56,"commentCount":56,"isTop":56,"isRecommend":56,"status":55,"rejectReason":5,"publishedAt":268,"createdAt":269,"updatedAt":270,"category":271,"tags":272},15,"FastAPI 快速上手：30 分钟写一个可部署的 REST API","fastapi","Python 写接口最快的框架。从第一个路由到参数校验、自动文档、数据库连接，再到生产部署，一篇讲完整条链路。","## 为什么选 FastAPI\n\nPython Web 框架里，Flask 简单但太原始，Django 功能全但太重。FastAPI 是新一代的选择：\n\n- **快**：性能接近 Node.js 和 Go（基于 Starlette + Pydantic）\n- **自动文档**：写完代码自动生成交互式 API 文档，前端看了直呼内行\n- **类型安全**：用 Python 类型注解做参数校验，类型错了直接返回 422\n- **异步原生**：天生支持 async\u002Fawait，IO 密集场景性能碾压\n\n```bash\npip install fastapi uvicorn\n```\n\n## 第一个接口：5 行代码\n\n```python\n# main.py\nfrom fastapi import FastAPI\n\napp = FastAPI(title=\"我的第一个API\")\n\n@app.get(\"\u002F\")\ndef hello():\n    return {\"message\": \"Hello FastAPI\"}\n\n@app.get(\"\u002Fitems\u002F{item_id}\")\ndef get_item(item_id: int):   # 声明 int 类型，自动转换和校验\n    return {\"item_id\": item_id, \"name\": f\"商品{item_id}\"}\n```\n\n启动：\n\n```bash\nuvicorn main:app --reload --port 8000\n```\n\n- `--reload`：改代码自动重启，开发必备\n- `main:app`：指 main.py 文件里的 app 对象\n\n现在访问：\n\n- `http:\u002F\u002F127.0.0.1:8000\u002Fitems\u002F42` → 返回 JSON\n- `http:\u002F\u002F127.0.0.1:8000\u002Fitems\u002Fabc` → 自动返回 422 错误（因为 abc 不是 int）\n- `http:\u002F\u002F127.0.0.1:8000\u002Fdocs` → **自动生成的 Swagger 文档页面**，可以直接在页面上测试接口\n\n## 参数校验：Pydantic 的威力\n\nPOST 请求的数据校验，用 Pydantic 模型声明：\n\n```python\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel, Field\n\napp = FastAPI()\n\nclass ItemCreate(BaseModel):\n    name: str = Field(min_length=1, max_length=50, description=\"商品名\")\n    price: float = Field(gt=0, description=\"价格必须大于0\")\n    stock: int = Field(default=0, ge=0)\n    tags: list[str] = []\n\n@app.post(\"\u002Fitems\")\ndef create_item(item: ItemCreate):\n    return {\"message\": \"创建成功\", \"data\": item}\n```\n\n发请求时：\n\n- 字段缺失\u002F类型错误\u002F不满足约束 → 自动返回详细的 422 错误信息\n- 一切正常 → `item` 是一个类型安全的对象，IDE 有完整提示\n\n**再也不用手写 `if not name: return error` 这类校验代码**。\n\n## 查询参数与路径参数\n\n```python\nfrom typing import Optional\n\n@app.get(\"\u002Fitems\")\ndef list_items(\n    keyword: Optional[str] = None,   # 可选查询参数 ?keyword=xxx\n    page: int = 1,                    # 带默认值\n    size: int = Field(default=20, le=100),  # 限制最大100\n):\n    return {\"keyword\": keyword, \"page\": page, \"size\": size}\n```\n\n## 一个完整的小项目：待办事项 API\n\n把前面的知识串起来，写一个带增删改查的完整 API：\n\n```python\n# todo.py\nfrom fastapi import FastAPI, HTTPException\nfrom pydantic import BaseModel, Field\n\napp = FastAPI(title=\"待办事项API\")\n\n# 用列表模拟数据库（下节换成真实数据库）\ntodos = []\nnext_id = 1\n\nclass TodoCreate(BaseModel):\n    title: str = Field(min_length=1, max_length=100)\n    done: bool = False\n\nclass Todo(TodoCreate):\n    id: int\n\n@app.get(\"\u002Ftodos\", response_model=list[Todo])\ndef list_todos():\n    \"\"\"获取所有待办\"\"\"\n    return todos\n\n@app.post(\"\u002Ftodos\", response_model=Todo, status_code=201)\ndef create_todo(item: TodoCreate):\n    \"\"\"创建待办\"\"\"\n    global next_id\n    todo = Todo(id=next_id, **item.model_dump())\n    next_id += 1\n    todos.append(todo)\n    return todo\n\n@app.get(\"\u002Ftodos\u002F{todo_id}\", response_model=Todo)\ndef get_todo(todo_id: int):\n    \"\"\"获取单个待办\"\"\"\n    for t in todos:\n        if t.id == todo_id:\n            return t\n    raise HTTPException(status_code=404, detail=\"待办不存在\")\n\n@app.put(\"\u002Ftodos\u002F{todo_id}\", response_model=Todo)\ndef update_todo(todo_id: int, item: TodoCreate):\n    \"\"\"更新待办\"\"\"\n    for i, t in enumerate(todos):\n        if t.id == todo_id:\n            todos[i] = Todo(id=todo_id, **item.model_dump())\n            return todos[i]\n    raise HTTPException(status_code=404, detail=\"待办不存在\")\n\n@app.delete(\"\u002Ftodos\u002F{todo_id}\", status_code=204)\ndef delete_todo(todo_id: int):\n    \"\"\"删除待办\"\"\"\n    for i, t in enumerate(todos):\n        if t.id == todo_id:\n            todos.pop(i)\n            return\n    raise HTTPException(status_code=404, detail=\"待办不存在\")\n```\n\n`response_model` 的好处：自动过滤返回值（比如不暴露内部字段）、生成准确的文档、做响应校验。\n\n## 连接真实数据库（以 SQLite + SQLAlchemy 为例）\n\n```bash\npip install sqlalchemy\n```\n\n```python\n# database.py\nfrom sqlalchemy import Column, Integer, String, Boolean, create_engine\nfrom sqlalchemy.orm import declarative_base, sessionmaker\n\nengine = create_engine(\"sqlite:\u002F\u002F\u002F.\u002Ftodos.db\", connect_args={\"check_same_thread\": False})\nSessionLocal = sessionmaker(bind=engine)\nBase = declarative_base()\n\nclass TodoModel(Base):\n    __tablename__ = \"todos\"\n    id = Column(Integer, primary_key=True, index=True)\n    title = Column(String(100), nullable=False)\n    done = Column(Boolean, default=False)\n\nBase.metadata.create_all(engine)  # 自动建表\n```\n\n在 FastAPI 中用依赖注入管理数据库会话：\n\n```python\nfrom fastapi import Depends\nfrom sqlalchemy.orm import Session\n\ndef get_db():\n    \"\"\"每个请求一个会话，结束自动关闭\"\"\"\n    db = SessionLocal()\n    try:\n        yield db\n    finally:\n        db.close()\n\n@app.get(\"\u002Ftodos\")\ndef list_todos(db: Session = Depends(get_db)):\n    return db.query(TodoModel).all()\n\n@app.post(\"\u002Ftodos\", status_code=201)\ndef create_todo(item: TodoCreate, db: Session = Depends(get_db)):\n    todo = TodoModel(**item.model_dump())\n    db.add(todo)\n    db.commit()\n    db.refresh(todo)\n    return todo\n```\n\n## 生产部署\n\n开发用 `--reload`，生产环境这样跑：\n\n```bash\n# 4 个 worker 进程，绑定所有网卡\nuvicorn main:app --host 0.0.0.0 --port 8000 --workers 4\n```\n\n更稳的方案是 `gunicorn` 管理 uvicorn worker（Linux）：\n\n```bash\npip install gunicorn\ngunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000\n```\n\n前面再挂 Nginx 反向代理，搞定域名和 HTTPS，就是标准的生产架构。\n\n## 进阶方向\n\n到这里你已经能写可用的 API 了。下一步按需学习：\n\n- **用户认证**：`fastapi-users` 或手写 JWT（`python-jose`）\n- **跨域 CORS**：前端联调必配 `CORSMiddleware`\n- **后台任务**：`BackgroundTasks` 处理发邮件这类慢操作\n- **限流**：`slowapi` 防止接口被刷\n- **测试**：`fastapi.testclient` 基于 httpx，写接口测试很顺手\n\n## 写在最后\n\nFastAPI 的设计哲学是「别让我重复写样板代码」。类型注解即校验、代码即文档——把这两个特性用足，你的接口开发效率会提升一个量级。建议把待办事项的例子亲手跑通，再把它改造成一个你自己真正需要的小工具。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790636138525796879.png","2026-09-29T06:56:37.777573439+08:00","2026-09-29T06:55:38.52593956+08:00","2026-09-29T06:56:37.777643867+08:00",{"id":120,"name":125,"slug":68,"description":125,"icon":5,"sortOrder":56,"postCount":56,"parentId":56,"createdAt":126,"updatedAt":126},[273,274,278,283,288],{"id":67,"name":68,"slug":69,"color":70,"postCount":56,"createdAt":71},{"id":275,"name":276,"slug":264,"color":70,"postCount":56,"createdAt":277},132,"FastAPI","2026-09-29T06:55:38.496902061+08:00",{"id":279,"name":280,"slug":281,"color":70,"postCount":56,"createdAt":282},133,"Web开发","web-kai-fa","2026-09-29T06:55:38.503802445+08:00",{"id":284,"name":285,"slug":286,"color":70,"postCount":56,"createdAt":287},134,"REST API","rest-api","2026-09-29T06:55:38.511505671+08:00",{"id":289,"name":290,"slug":291,"color":70,"postCount":56,"createdAt":292},135,"后端","hou-duan","2026-09-29T06:55:38.519097242+08:00",{"newer":294,"older":304},{"id":67,"title":295,"slug":296,"summary":297,"content":298,"coverImage":299,"categoryId":300,"authorId":55,"viewCount":56,"likeCount":56,"commentCount":56,"isTop":56,"isRecommend":56,"status":55,"rejectReason":5,"publishedAt":301,"createdAt":302,"updatedAt":303},"RAG 实战：从零搭建一个本地知识库问答系统","rag-knowledge-base","大模型不知道你的私有文档怎么办？用 RAG 技术把本地文档喂给 AI。从原理到代码，手把手用 Python + LangChain 搭建一个可运行的知识库问答系统。","## 为什么需要 RAG\n\n大模型有两个天然的短板：\n\n1. **知识截止**：训练数据有截止日期，之后的事它不知道\n2. **不懂私有信息**：你公司的内部文档、你的产品手册，它从没见过\n\n把全部文档塞进提示词？不行——一份几百页的手册远超上下文窗口限制，而且费用爆炸。\n\n**RAG（检索增强生成）** 的思路很朴素：**先从文档库里找出相关的几段，再让 AI 基于这几段内容回答**。相当于开卷考试——AI 不需要背下整本书，只要会查资料就行。\n\n## RAG 工作原理拆解\n\n整个系统分两个阶段：\n\n**入库阶段（离线做一次）**：\n\n```text\n文档 → 切分成小块 → 每块算一个向量（Embedding）→ 存进向量数据库\n```\n\n**问答阶段（每次提问）**：\n\n```text\n用户问题 → 算问题的向量 → 在向量库里找最相似的几个文档块\n→ 把「问题 + 找到的文档块」一起发给大模型 → 生成回答\n```\n\n核心概念只有三个：\n\n- **Embedding（向量嵌入）**：把一段文字变成一串数字（如 1536 维的向量），语义相近的文字向量距离也近。「猫」和「猫咪」的向量很近，「猫」和「汽车」很远\n- **向量数据库**：专门存向量并支持「找最相似」查询的数据库，轻量级的有 Chroma、FAISS\n- **检索召回**：根据问题找出最相关的 top-k 个文档块（一般取 3-5 个）\n\n## 环境准备\n\n```bash\npip install langchain langchain-community langchain-text-splitters \\\n    chromadb sentence-transformers openai\n```\n\n说明：\n\n- `sentence-transformers`：本地跑 Embedding，免费且不用调 API\n- `chromadb`：轻量向量数据库，数据存本地文件，零配置\n- `openai`：调用大模型（可换成 DeepSeek 等兼容接口，下文以 DeepSeek 为例，便宜）\n\n## 完整代码（约 60 行，可直接运行）\n\n### 第一步：文档入库\n\n```python\n# build_kb.py —— 把 docs\u002F 目录下的所有 txt\u002Fmd 文档存入向量库\nfrom pathlib import Path\n\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_community.embeddings import HuggingFaceEmbeddings\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\n# 1. 读取本地文档\ndocs_dir = Path(\"docs\")  # 把你的 txt\u002Fmd 文件放这个目录\ntexts, metadatas = [], []\nfor f in docs_dir.rglob(\"*\"):\n    if f.suffix in (\".txt\", \".md\"):\n        texts.append(f.read_text(encoding=\"utf-8\"))\n        metadatas.append({\"source\": f.name})\n\n# 2. 切分文档：每块约 500 字，块间重叠 50 字防止切断语义\nsplitter = RecursiveCharacterTextSplitter(\n    chunk_size=500, chunk_overlap=50, separators=[\"\\n\\n\", \"\\n\", \"。\", \"！\", \"？\"]\n)\nall_chunks = []\nfor text, meta in zip(texts, metadatas):\n    for chunk in splitter.split_text(text):\n        all_chunks.append((chunk, meta))\n\n# 3. 加载本地 Embedding 模型（首次运行自动下载，约 400MB）\nembeddings = HuggingFaceEmbeddings(model_name=\"BAAI\u002Fbge-small-zh-v1.5\")\n\n# 4. 写入向量数据库（持久化到 .\u002Fchroma_db 目录）\ndb = Chroma(\n    collection_name=\"my_kb\",\n    embedding_function=embeddings,\n    persist_directory=\".\u002Fchroma_db\",\n)\ndb.add_texts(\n    texts=[c for c, _ in all_chunks],\n    metadatas=[m for _, m in all_chunks],\n)\nprint(f\"入库完成，共 {len(all_chunks)} 个文档块\")\n```\n\n运行一次即可，之后问答不需要重新入库。\n\n### 第二步：问答\n\n```python\n# ask.py —— 基于知识库回答问题\nfrom openai import OpenAI\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_community.embeddings import HuggingFaceEmbeddings\n\n# 加载已有的向量库\nembeddings = HuggingFaceEmbeddings(model_name=\"BAAI\u002Fbge-small-zh-v1.5\")\ndb = Chroma(\n    collection_name=\"my_kb\",\n    embedding_function=embeddings,\n    persist_directory=\".\u002Fchroma_db\",\n)\n\n# DeepSeek 的兼容 OpenAI 接口（换成你自己的 key）\nclient = OpenAI(\n    api_key=\"sk-你的key\",\n    base_url=\"https:\u002F\u002Fapi.deepseek.com\",\n)\n\ndef ask(question: str) -> str:\n    # 1. 检索：找最相关的 4 个文档块\n    docs = db.similarity_search(question, k=4)\n    context = \"\\n\\n\".join(\n        f\"【来源：{d.metadata.get('source', '未知')}】\\n{d.page_content}\"\n        for d in docs\n    )\n\n    # 2. 组装提示词：把检索结果作为参考资料\n    prompt = f\"\"\"请仅根据下面的参考资料回答问题。\n如果资料里没有相关信息，直接回答「知识库中没有相关内容」，不要编造。\n\n【参考资料】\n{context}\n\n【问题】{question}\n\n回答时请注明信息来源文件名。\"\"\"\n\n    # 3. 调用大模型生成回答\n    resp = client.chat.completions.create(\n        model=\"deepseek-chat\",\n        messages=[{\"role\": \"user\", \"content\": prompt}],\n        temperature=0.1,  # 低温减少发挥，让答案更贴资料\n    )\n    return resp.choices[0].message.content\n\nif __name__ == \"__main__\":\n    while True:\n        q = input(\"\\n请输入问题（q 退出）：\").strip()\n        if q.lower() == \"q\":\n            break\n        print(\"\\n\" + ask(q))\n```\n\n## 实测效果\n\n把本站的 40 篇 AI 工具介绍文档放进 `docs\u002F` 目录入库后：\n\n- 问「有哪些免费的 AI 绘画工具？」→ 回答准确列出免费工具并注明来源文件\n- 问「Midjourney 多少钱一个月？」→ 从文档里找到价格信息作答\n- 问「怎么申请高新技术企业认定？」→ 如实回答「知识库中没有相关内容」——这正是我们要的效果，**宁可说不知道，也不许瞎编**\n\n## 调优方向（效果不满意时看这里）\n\n**检索不准** → 调切分策略：`chunk_size` 太小会切断语义，太大会稀释相关性，500 字左右通常合适；也可以换更强的 Embedding 模型（如 `bge-large-zh-v1.5`）。\n\n**回答啰嗦\u002F跑题** → 调提示词：强调「仅根据参考资料」；把 `temperature` 降到 0。\n\n**召回的内容不全** → 把 `k=4` 调大到 6-8，注意别超过模型上下文。\n\n**想支持 PDF\u002FWord** → 用 `langchain-community` 的文档加载器：\n\n```python\nfrom langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader\n\ndocs = PyPDFLoader(\"手册.pdf\").load()        # 加载 PDF\ndocs = Docx2txtLoader(\"文档.docx\").load()    # 加载 Word\n```\n\n## 生产环境还需要什么\n\n上面的 60 行代码是「能跑的 demo」，真正上线还要考虑：\n\n- **权限隔离**：不同用户只能检索自己有权限的文档（按 metadata 过滤）\n- **增量更新**：文档变更后只更新对应块，不要全量重建\n- **混合检索**：向量检索 + 关键词检索（BM25）融合，兼顾语义和精确匹配\n- **评估体系**：准备一批标准问答对，量化每次调优的效果\n\n但对个人知识库、小团队内部问答场景，本文这套方案已经够用。把代码跑起来，喂给它你的第一批文档吧。\n","\u002Fstatic\u002Fuploads\u002Fimages\u002F2026\u002F09\u002F29\u002F1790635573941737564.png",4,"2026-09-29T06:46:36.332298489+08:00","2026-09-29T06:46:13.941901418+08:00","2026-09-29T06:46:36.332355916+08:00",null,{"items":306,"pagination":307},[],{"page":55,"pageSize":114,"total":56,"totalPages":55},{"materials":309},[],{"materials":311},[]]