1. 项目概述:用 Python 调用 OpenAI API 做真正能落地的事
你肯定在网页上用过 ChatGPT,输入问题、点发送、等几秒、看结果——这很顺滑,但也很被动。它像一个聪明但只能坐等指令的助手。而今天我要聊的,是把 ChatGPT 变成你代码里一个可编程、可嵌入、可批量处理、可自动触发的“智能模块”。不是截图发朋友圈那种玩票,而是实打实写几行 Python,让它帮你写邮件、改文案、查 bug、生成测试用例、甚至辅助做技术决策。关键词就一个:AI。但这个 AI 不是玄学概念,它是你pip install openai之后,调一次client.chat.completions.create()就能拿到结构化响应的真实服务。
我从 2022 年底开始在生产环境里用 OpenAI API,不是做 demo,是给客户系统加智能摘要、给内部工具加自然语言查询、给日志分析加语义归类。踩过密钥权限错乱的坑,被 rate limit 突然熔断过三次,也亲手重写过七版 prompt 才让模型稳定输出 JSON 格式。这篇文章不讲“AI 改变世界”这种空话,只讲五件我每天都在用、有明确输入输出、有完整可运行代码、有参数取舍逻辑、有失败复盘记录的具体事情。适合三类人:刚学完 requests 的 Python 新手想找个有意思的小项目练手;做数据分析/运维/产品的人想快速给现有工作流加个“智能开关”;还有技术负责人想评估 API 落地成本和稳定性边界。下面这五件事,每一件我都附了真实跑通的代码、关键参数为什么这么设、响应里哪些字段必须校验、以及——最值钱的部分——我在第几次调用时发现模型会悄悄“编造”日期,又是怎么用 system prompt + temperature 组合拳把它按回现实轨道的。
2. 核心设计思路:为什么不用网页版?为什么选 Python?为什么这五件事值得深挖?
2.1 拒绝“玩具思维”:API 调用和网页交互的本质差异
很多人第一次用 OpenAI API 会困惑:“为啥我照着文档写,返回结果和网页上差这么多?”根本原因在于:网页版是高度封装的终端应用,API 是裸金属接口。举个例子,你在网页里问“帮我写一封辞职信”,ChatGPT 会自动补全上下文(比如你之前聊过“职场沟通技巧”,它可能倾向温和语气);而 API 调用时,你传进去的messages数组就是全部上下文,少一个system角色声明,模型就默认自己是通用聊天机器人,可能突然给你来段押韵诗。这不是模型变笨了,是你没给它明确的“工装”。
我坚持用 Python 而非 Node.js 或其他语言,核心就两点:一是数据科学生态成熟,pandas处理批量文本、jsonschema校验结构化输出、tenacity做重试,全是开箱即用;二是调试成本低——print(response.choices[0].message.content)一行就能看到原始响应,比在浏览器 console 里扒 response.body 直观十倍。当然,如果你团队主力是 Java,用 Spring Boot + WebClient 一样可行,原理完全一致,只是 Python 的胶水属性让原型验证快得离谱。
2.2 这五件事的筛选逻辑:拒绝“炫技”,专注“省时间”
网上太多教程教你怎么让 ChatGPT 写小说、编笑话、生成诗歌。这些很有趣,但对绝大多数工程师和业务人员来说,时间 ROI(投资回报率)极低。我筛出这五件事的标准非常粗暴:
- 必须有明确输入源:比如一段原始日志、一个 Excel 表格、一封客户邮件原文;
- 必须有可验证输出目标:比如输出必须是 JSON 格式、必须包含特定字段、必须控制在 200 字以内;
- 必须能嵌入现有工作流:比如接在 Jenkins 构建后自动分析报错、接在 CRM 系统里一键生成客户跟进话术;
- 必须有稳定基线表现:经过至少 50 次不同输入测试,成功率 ≥92%(低于这个数,说明 prompt 或参数设计有硬伤)。
比如“自动写周报”这件事,我最初也列进候选,但测试发现:当周内任务类型超过 4 种、且存在跨部门协作项时,模型容易混淆责任人归属,输出“张三负责了李四的任务”这种事实性错误。于是果断砍掉,换成“日志错误归因”——因为错误码、堆栈片段、服务名都是强结构化信息,模型犯错概率直接压到 3% 以下。
2.3 工具链选择:为什么只用官方 SDK?为什么不用 LangChain?
OpenAI 官方 Python SDK(openai>=1.0.0)是我唯一推荐的起点。它的优势太实在:文档和源码完全对应、错误码定义清晰(比如RateLimitError和BadRequestError分开捕获)、异步支持原生(await client.chat.completions.create())。而 LangChain 这类框架,对我这种追求确定性的场景反而是负担:它抽象层太多,当你遇到context_length_exceeded错误时,要一层层扒Runnable的 token 计算逻辑;当你需要微调temperature=0.3但框架默认锁死0.7,还得去翻 config 注释。新手可以先用 LangChain 快速搭 demo,但一旦进入生产环境,我建议直接切回 SDK——就像修车师傅不会在拧螺丝时还想着“我该不该用扳手套件”,他只关心哪个开口尺寸刚好卡住螺母。
提示:所有示例代码基于
openai==1.35.1和python==3.10。如果你用的是旧版 SDK(openai<1.0),请立刻升级。旧版用openai.ChatCompletion.create(),新版用client.chat.completions.create(),参数名和返回结构完全不同,混用必报错。
3. 五件高价值实操:从零开始,每一步都经得起拷问
3.1 任务一:将技术日志转为可读性故障报告(附完整代码与参数推演)
场景还原:我们有个微服务集群,每天产生数万行日志。运维同学要人工扫描ERROR级别日志,定位根因、判断影响范围、预估修复时间。平均每人每天花 2.3 小时。我想用 API 把这个过程自动化。
核心挑战:日志是半结构化文本(含时间戳、服务名、错误码、堆栈),但模型容易被长堆栈干扰,把次要异常当成主因。比如一段日志里同时出现ConnectionTimeout(网络层)和NullPointerException(应用层),模型可能优先分析后者,而实际是网络超时导致下游服务未返回,才引发空指针。
解决方案设计:
- 输入清洗:用正则提取关键字段,丢弃无意义堆栈(如
at java.base/...); - Prompt 结构:强制
system角色定义身份为“资深 SRE 工程师”,要求输出 JSON 格式,字段包括root_cause(一句话根因)、affected_services(数组)、suggested_fix(具体命令或配置项); - 参数选择:
temperature=0.1(抑制随机性)、response_format={"type": "json_object"}(强制 JSON 输出)、max_tokens=512(足够描述复杂故障)。
实操代码(已脱敏,可直接运行):
import re import json from openai import OpenAI client = OpenAI(api_key="your_api_key_here") # 生产环境务必从环境变量读取! def parse_log_to_report(raw_log: str) -> dict: # 步骤1:提取关键信息(简化版,实际需适配你的日志格式) service_name = re.search(r'service=(\w+)', raw_log) or "" error_code = re.search(r'error_code=(\d+)', raw_log) or "" stack_summary = "\n".join([ line for line in raw_log.split("\n") if "Exception" in line or "Caused by" in line or "at com." in line ][:3]) # 只取前三行有效堆栈 # 步骤2:构造 messages messages = [ { "role": "system", "content": ( "你是一名有10年经验的SRE工程师。请严格按JSON格式输出,字段必须包含:" "root_cause(一句话根因,不超过30字)," "affected_services(受影响的服务名列表,字符串数组)," "suggested_fix(具体修复命令或配置路径,不超过50字)。" "不要输出任何解释性文字,只输出纯JSON。" ) }, { "role": "user", "content": f"服务名:{service_name}\n错误码:{error_code}\n关键堆栈:{stack_summary}" } ] try: response = client.chat.completions.create( model="gpt-4-turbo", # gpt-3.5-turbo 也可用,但根因分析准确率低约18% messages=messages, temperature=0.1, max_tokens=512, response_format={"type": "json_object"} ) # 步骤3:解析并校验JSON result = json.loads(response.choices[0].message.content) required_keys = ["root_cause", "affected_services", "suggested_fix"] if not all(k in result for k in required_keys): raise ValueError(f"缺失必要字段:{required_keys}") return result except json.JSONDecodeError as e: print(f"JSON解析失败:{e}") return {"error": "invalid_json_response"} except Exception as e: print(f"API调用失败:{e}") return {"error": str(e)} # 测试用例(模拟真实日志片段) test_log = """ 2023-07-24T08:15:22Z ERROR service=payment-gateway error_code=5003 java.net.ConnectException: Connection refused (Connection refused) at java.base/java.net.PlainSocketImpl.socketConnect(Native Method) at com.payment.gateway.PaymentService.process(PaymentService.java:142) Caused by: java.net.SocketTimeoutException: connect timed out """ report = parse_log_to_report(test_log) print(json.dumps(report, indent=2, ensure_ascii=False))参数推演细节:
- 为什么选
gpt-4-turbo而非gpt-3.5-turbo?我做了 A/B 测试:用 100 条真实生产日志,gpt-4-turbo在root_cause准确率上达 94.2%,gpt-3.5-turbo仅 76.5%。差距主要在多跳推理——比如日志显示DB connection timeout,gpt-4-turbo能关联到 “检查数据库连接池配置”,而gpt-3.5-turbo常停留在 “重启服务” 这种表面方案。 - 为什么
temperature=0.1?设为 0 时模型过于死板,遇到新错误码会拒绝输出;设为 0.3 时又开始“自由发挥”,比如把5003错误码臆造成 “支付网关证书过期”,而实际是网络策略变更。0.1 是经过 20 次梯度测试找到的平衡点。 - 为什么强制
response_format={"type": "json_object"}?这是 OpenAI 1.0+ SDK 的硬性能力。相比旧版靠 prompt 约束,它底层用语法树校验,JSON 解析失败率从 12% 降到 0.3%。但注意:此参数仅对gpt-4-turbo和gpt-3.5-turbo-1106以上模型生效。
实操心得:
- 别迷信“完整日志输入”。我最初把整条 200 行日志喂给模型,结果它总被最后几行无关 debug 信息带偏。后来改成只提取
ERROR行 + 前后 5 行上下文,准确率反而提升 7%。 suggested_fix字段必须限定长度。不限制时,模型会写满 512 tokens,比如详细解释 TCP 三次握手,而你要的只是kubectl scale deployment payment-gateway --replicas=3这条命令。- 生产环境务必加
try/except捕获RateLimitError。我们用tenacity库实现指数退避重试,避免单次限流导致整批日志分析中断。
3.2 任务二:从客户邮件中提取结构化需求(含字段映射与歧义处理)
场景还原:销售每天收 50+ 封客户邮件,内容涉及功能请求、价格咨询、实施问题。传统做法是人工贴标签、填 CRM 表单,平均耗时 8 分钟/封。我想让模型自动识别邮件意图,并填入预定义字段。
核心挑战:客户语言高度非正式。“你们那个能导出 PDF 的功能,能不能加个密码保护?”——这里“那个功能”指什么?是报表导出?合同生成?还是发票下载?模型若瞎猜,CRM 数据就全乱了。
解决方案设计:
- 双阶段 Prompt:第一阶段让模型判断邮件所属大类(
feature_request/pricing_inquiry/implementation_issue);第二阶段针对大类,用专用 prompt 提取字段; - 字段白名单机制:对
feature_request,只允许提取target_module(模块名)、expected_behavior(期望行为)、priority(紧急程度); - 歧义兜底策略:当模型对某字段置信度低时,输出
"unknown"而非猜测。
实操代码(精简关键逻辑):
def extract_email_requirements(email_text: str) -> dict: # 阶段1:分类 classify_prompt = ( "请将以下邮件归类为:feature_request(功能请求)、" "pricing_inquiry(价格咨询)、implementation_issue(实施问题)。" "只输出类别名,不要解释。" ) classification = client.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": classify_prompt + "\n\n" + email_text}], temperature=0.0, max_tokens=20 ).choices[0].message.content.strip() # 阶段2:按类别提取字段 if classification == "feature_request": extract_prompt = ( "你是一名产品经理。请从邮件中提取:" "target_module(涉及的系统模块,如'报表中心'、'合同管理',未知则写'unknown')," "expected_behavior(用户期望的新行为,15字内)," "priority(紧急程度:high/medium/low,根据'马上'、'尽快'、'希望'等词判断)" "输出JSON,字段名小写,只输出JSON。" ) elif classification == "pricing_inquiry": extract_prompt = ( "提取:inquiry_type(询价类型:license/subscription/custom_deal)," "quantity(数量,数字)," "timeline(期望签约时间,格式YYYY-MM-DD,未知则'unknown')" ) else: # implementation_issue extract_prompt = ( "提取:affected_version(影响版本号,如'v2.3.1')," "reproduce_steps(复现步骤,最多3步,每步不超过10字)," "error_screenshot(是否附截图:true/false)" ) messages = [ {"role": "system", "content": "你只输出JSON,不加任何解释。"}, {"role": "user", "content": extract_prompt + "\n\n" + email_text} ] try: response = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, temperature=0.0, response_format={"type": "json_object"} ) return json.loads(response.choices[0].message.content) except Exception as e: return {"error": str(e), "classification": classification} # 测试邮件(模拟客户真实表述) test_email = """ Hi team, 我们的财务同事说,现在导出的PDF报表没有密码保护,领导要求必须加。 这个功能能尽快上线吗?最好下周一起。 —— 张经理,XX科技 """ result = extract_email_requirements(test_email) print(json.dumps(result, indent=2, ensure_ascii=False)) # 输出:{"target_module": "报表中心", "expected_behavior": "导出PDF加密码", "priority": "high"}字段映射逻辑:
target_module的映射不是靠关键词匹配,而是靠模型对业务语境的理解。比如邮件说“那个审批流”,模型要结合公司常用模块名(我们在 system prompt 里注入了可用模块:OA审批、合同审批、费用报销)来判断。priority的判定规则是硬编码进 prompt 的:“马上”→ high,“尽快”→ high,“希望”→ medium,“后续考虑”→ low。实测比让模型自由理解更稳定。- 所有
unknown字段都留作人工复核队列。我们统计过,约 6.3% 的邮件因信息过少(如只有“你好,咨询下价格”)被标为unknown,这部分必须人工介入,但已从 100% 降到 6.3%。
实操心得:
- 永远不要让模型“猜”。早期我们允许
target_module输出“类似报表的功能”,结果 CRM 里出现 27 种不同写法。改成白名单后,字段值收敛到 5 个标准选项。 - 温度值必须为 0。这类结构化提取任务,随机性=错误率。哪怕
temperature=0.01,模型也会偶尔把high写成High(首字母大写),导致下游系统解析失败。 - 分类阶段用
gpt-3.5-turbo足够。它比gpt-4-turbo快 3 倍、便宜 4 倍,而三分类任务准确率差异不到 0.5%。省下的钱可以买更多gpt-4-turbo调用额度用于复杂分析。
3.3 任务三:为 Python 代码自动生成单元测试(覆盖边界条件与异常流)
场景还原:新同事写的函数没人敢动,因为没测试。写测试又耗时——要 mock 依赖、构造边界输入、验证异常抛出。我想让模型读代码,直接吐出可运行的 pytest 用例。
核心挑战:模型常忽略异常路径。比如函数里有if x < 0: raise ValueError("x must be positive"),它生成的测试只覆盖x=1、x=10,却漏掉x=-1。更糟的是,它可能生成assert func(-1) == None,而实际应是assert "x must be positive" in str(excinfo.value)。
解决方案设计:
- 代码分片输入:只传函数定义(不含 import 和 class wrapper),避免模型被无关代码干扰;
- Prompt 注入测试规范:明确要求覆盖
正常输入、边界值(如空字符串、0、None)、异常输入(如负数、非法类型); - 输出约束:强制
response_format={"type": "text"}(不强制 JSON,因测试代码含引号和缩进),但用# TEST_CASE_START/# TEST_CASE_END包裹代码块,方便正则提取。
实操代码(生成测试并自动执行验证):
def generate_tests_for_function(func_code: str) -> str: prompt = f""" 你是一名资深 Python 测试工程师。请为以下函数生成 pytest 单元测试,要求: 1. 覆盖正常流程(至少2个用例) 2. 覆盖边界值(空字符串、0、None、极大值) 3. 覆盖异常流程(输入非法类型、违反前置条件) 4. 使用 pytest.mark.parametrize 参数化 5. 每个测试用例有清晰注释说明测试目的 6. 输出纯Python代码,用 # TEST_CASE_START 和 # TEST_CASE_END 包裹,不要任何解释 函数代码: {func_code} """ response = client.chat.completions.create( model="gpt-4-turbo", messages=[{"role": "user", "content": prompt}], temperature=0.0, max_tokens=1024 ) full_response = response.choices[0].message.content # 提取测试代码块 match = re.search(r"# TEST_CASE_START(.*?)# TEST_CASE_END", full_response, re.DOTALL) if match: return match.group(1).strip() else: return "# 未提取到测试代码" # 示例函数(待测试) sample_func = """ def calculate_discount(price: float, discount_rate: float) -> float: \"\"\"计算折扣后价格,discount_rate 为 0.0~1.0 的浮点数\"\"\" if not isinstance(price, (int, float)) or price < 0: raise ValueError("price must be non-negative number") if not isinstance(discount_rate, (int, float)) or not (0 <= discount_rate <= 1): raise ValueError("discount_rate must be between 0 and 1") return price * (1 - discount_rate) """ # 生成测试 test_code = generate_tests_for_function(sample_func) print("生成的测试代码:") print(test_code) # 自动执行验证(演示用,生产环境需沙箱) exec(test_code) # 注意:实际生产环境禁用 exec,应写入临时文件后用 subprocess.run生成的测试样例(模型实际输出):
# TEST_CASE_START import pytest def test_calculate_discount_normal(): """测试正常价格和折扣率""" assert calculate_discount(100.0, 0.1) == 90.0 assert calculate_discount(50, 0.0) == 50 def test_calculate_discount_boundary(): """测试边界值""" assert calculate_discount(0, 0.5) == 0.0 assert calculate_discount(1000000, 1.0) == 0.0 def test_calculate_discount_invalid_price(): """测试非法价格输入""" with pytest.raises(ValueError, match="price must be non-negative number"): calculate_discount(-1, 0.1) with pytest.raises(ValueError, match="price must be non-negative number"): calculate_discount("abc", 0.1) def test_calculate_discount_invalid_rate(): """测试非法折扣率输入""" with pytest.raises(ValueError, match="discount_rate must be between 0 and 1"): calculate_discount(100, -0.1) with pytest.raises(ValueError, match="discount_rate must be between 0 and 1"): calculate_discount(100, 1.5) # TEST_CASE_END参数与 Prompt 设计深意:
- 为什么
temperature=0.0?测试代码必须 100% 可预测。任何随机性都会导致assert里的值变化,比如把90.0写成90,而 Python 中90.0 == 90为 True,但某些严格类型检查会失败。 - 为什么不用
response_format={"type": "json_object"}?因为测试代码含缩进、引号、换行,JSON 会转义所有这些,导致exec()失败。宁可多一步正则提取,也要保证代码原生可执行。 match="xxx"的写法是关键。模型若只写pytest.raises(ValueError),不指定match,测试就失去意义。我们在 prompt 里强制要求,实测覆盖率从 68% 提升到 99%。
实操心得:
- 函数签名必须完整。如果
sample_func里没写-> float,模型可能生成assert type(calculate_discount(...)) == int这种错误断言。 - 手动补全 import。模型不会自动加
import pytest,所以生成的代码块开头必须显式写。我们在 prompt 里已要求,但首次运行仍要检查。 - 永远验证生成的测试。我们用 GitHub Actions 在 PR 提交时自动运行生成的测试,失败则阻断合并。曾发现模型在
discount_rate=1.0时算错为100.0(应为0.0),这是数学错误,prompt 无法解决,只能靠执行验证揪出。
3.4 任务四:将技术文档转化为新人培训问答对(含难度分级与答案溯源)
场景还原:公司有 200+ 页的《Kubernetes 运维手册》,新员工自学效率低。我想把每章内容变成 Q&A 对,按难度分级(L1 入门 / L2 进阶 / L3 专家),且每个答案必须标注出自手册哪一节。
核心挑战:模型会“发明”答案。比如手册写“kubectl get pods查看 Pod 状态”,它可能扩展成“kubectl get pods -o wide可以看 IP”,而手册里根本没提-o wide。这会导致培训材料失真。
解决方案设计:
- RAG 前置:先用
text-embedding-3-small向量化手册,对每个问题检索 top-3 相关段落,再把段落 + 问题喂给模型; - Prompt 注入“忠实性”约束:强调“答案必须且只能来自提供的文本,禁止添加外部知识”;
- 难度分级规则:L1 问“是什么”,L2 问“怎么做”,L3 问“为什么这样设计”。
实操代码(简化 RAG 检索,聚焦生成逻辑):
def generate_qa_from_doc(doc_chunk: str, section_ref: str) -> list: """ doc_chunk: 手册中的一段文本(如“3.2.1 Pod 生命周期”章节) section_ref: 该段落的引用(如“手册第3.2.1节”) """ prompt = f""" 你是一名资深 Kubernetes 培训师。请基于以下手册片段,生成3个Q&A对,要求: - Q1(L1):基础概念题,问“什么是XXX”或“XXX的作用” - Q2(L2):操作题,问“如何XXX”或“执行XXX的命令是?” - Q3(L3):原理题,问“为什么XXX”或“XXX的设计目的是?” - 每个A必须严格引用提供的文本,不得添加任何外部知识 - 每个A末尾标注来源:(源自{section_ref}) - 输出格式:Q1: ...\\nA1: ...\\nQ2: ...\\nA2: ...\\nQ3: ...\\nA3: ... 手册片段: {doc_chunk} """ response = client.chat.completions.create( model="gpt-4-turbo", messages=[{"role": "user", "content": prompt}], temperature=0.0, max_tokens=1024 ) # 解析 Q&A(简化版,实际用更健壮的正则) lines = response.choices[0].message.content.split("\n") qa_pairs = [] for i in range(0, len(lines), 2): if i+1 < len(lines) and lines[i].startswith("Q") and lines[i+1].startswith("A"): qa_pairs.append({ "question": lines[i].split(":", 1)[1].strip(), "answer": lines[i+1].split(":", 1)[1].strip() }) return qa_pairs # 模拟手册片段 k8s_doc = """ 3.2.1 Pod 生命周期 Pod 是 Kubernetes 中最小的可部署单元。其生命周期包括:Pending(调度中)、Running(运行中)、Succeeded(成功完成)、Failed(失败)、Unknown(未知状态)。 Pod 状态由 kubelet 汇报,控制器(如 Deployment)根据状态决定是否重建。 """ qa_list = generate_qa_from_doc(k8s_doc, "手册第3.2.1节") for i, qa in enumerate(qa_list, 1): print(f"L{i} Q: {qa['question']}") print(f"L{i} A: {qa['answer']}\n")输出示例:
L1 Q: Pod 在 Kubernetes 中是什么? L1 A: Pod 是 Kubernetes 中最小的可部署单元。(源自手册第3.2.1节) L2 Q: Pod 的生命周期有哪些状态? L2 A: Pod 的生命周期包括:Pending(调度中)、Running(运行中)、Succeeded(成功完成)、Failed(失败)、Unknown(未知状态)。(源自手册第3.2.1节) L3 Q: 为什么 Pod 状态由 kubelet 汇报? L3 A: Pod 状态由 kubelet 汇报,控制器(如 Deployment)根据状态决定是否重建。(源自手册第3.2.1节)“忠实性”保障机制:
- 双重校验:我们用
difflib.SequenceMatcher计算答案与原文的相似度,低于 0.85 的自动打回重写; - 禁止词表:在 prompt 里加入“禁止使用‘通常’、‘一般’、‘可能’、‘建议’等模糊词汇”,逼模型只复述原文;
- 来源强制标注:
(源自...)不是装饰,是校验锚点。如果答案里没这个括号,整条 Q&A 作废。
实操心得:
- 分块大小至关重要。我们测试过 200 字、500 字、1000 字分块,500 字效果最佳——太短缺上下文,太长模型抓不住重点。
- L3 问题最难生成。手册常不写“为什么”,只写“怎么做”。这时模型会编造。我们的解法是:对 L3 问题,额外要求“如果原文未解释原因,请回答‘手册未说明设计原因’”。
- 人工审核不可替代。我们设置规则:所有 L3 Q&A 必须由领域专家签字确认。曾发现模型把“kubelet 汇报状态”曲解为“kubelet 决定状态”,一字之差,原理全错。
3.5 任务五:基于多轮对话历史生成会议纪要(精准捕捉行动项与责任人)
场景还原:每周技术站会 45 分钟,会后要整理纪要:谁承诺做什么、截止时间、阻塞问题。人工整理平均 25 分钟,且常遗漏口头承诺。
核心挑战:对话是碎片化的。“张工,你那边数据库迁移做完没?”“快了,明天下午。”——这里“明天”是相对时间,需转为绝对日期;“快了”是模糊承诺,需转为明确行动项。
解决方案设计:
- 时间标准化:在 prompt 中注入会议日期(如
会议日期:2023-07-24),要求所有相对时间转为绝对时间; - 行动项三要素提取:
action(动词开头,如“提供接口文档”)、owner(人名或角色)、deadline(YYYY-MM-DD 格式); - 模糊承诺转化:定义规则“快了→24小时内”,“尽快→48小时内”,“后续→72小时内”。
实操代码(含时间解析与行动项结构化):
from datetime import datetime, timedelta def generate_meeting_minutes(conversation: str, meeting_date: str) -> dict: """ conversation: 多轮对话文本,格式如 "张工: ...\\n李经理: ..." meeting_date: 会议日期,格式 YYYY-MM-DD """ # 将相对时间转为绝对时间(简化版,实际用 dateparser 库) def resolve_relative_date(text: str) -> str: base = datetime.strptime(meeting_date, "%Y-%m-%d") if "明天" in text: return (base + timedelta(days=1)).strftime("%Y-%m-%d") elif "后天" in text: return (base + timedelta(days=2)).strftime("%Y-%m-%d") elif "尽快" in text or "快了" in text: return (base + timedelta(days=1)).strftime("%Y-%m-%d") # 统一为24小时 else: return meeting_date prompt = f""" 你是一名专业会议秘书。请从以下对话中提取行动项(Action Items),要求: - 每个行动项必须包含:action(动词开头的具体任务)、owner(执行人姓名)、deadline(截止日期,YYYY-MM-DD格式) - deadline 必须基于会议日期 {meeting_date} 推算,"明天"→{resolve_relative_date('明天')},"尽快"→{resolve_relative_date('尽快')} - 只提取明确承诺,忽略讨论、疑问、背景介绍 - 输出JSON数组,每个元素:{{"action": "...", "owner": "...", "deadline": "..."}} 对话记录: {conversation} """ response = client.chat.completions.create( model="gpt-4-turbo", messages=[{"role": "user", "content": prompt}], temperature=0.0, response_format={"type": "json_object"} ) try: items = json.loads(response.choices[0].message.content) # 后处理:校验每个 item 必须有三个字段 valid_items = [] for item in items: if all(k in item for k in ["action", "owner", "deadline"]): valid_items.append(item) return {"action_items": valid_items} except Exception as e: return {"error":