一、提示工程(Prompt Engineering) 1.1 关键概念
Prompt(提示):给定的一段文本或指令,用于启动或引导AI模型响应。
Prompt Design(提示设计):设计良好的提示,目的是帮助模型更好理解输入的内容。
Few-shot Learning(少样本学习):提供给模型几个示例,让模型学习并应用于新的任务或数据点上。
Zero-shot Learning(零样本学习):不提供任何示例,仅通过明确的指令或描述来引导模型完成任务。
In-context Learning(基于上下文的学习):利用直接在提示中提供的上下文信息来进行学习和推理。
Chain-of-thought Prompting(思维链提示):通过在提示中加入中间步骤或思考过程来引导模型产生更复杂的推理和回答。
1.2 示例 1.2.1 输入问题 1 2 3 4 5 6 7 8 9 10 { 'problem' : '有一群人和一些食物类型。下列是关于这些个体和食物的已知信息:\n\n1. 鸡肉是一种食物。\n2. 苹果是一种食物。\n3. 如果X吃了Y,且X活着,则Y是一种食物。\n4. Bill存活。\n5. Bill吃了花生。\n6. John吃所有食物。\n7. Sue吃所有Bill吃的食物。\n8. John喜欢所有食物。\n\n根据以上信息,回答以下选择题:' , 'questions' : [ { 'question' : '选择题 1:\n谁喜欢吃花生?' , 'options' : ['Bill' , 'Sue' , 'John' , 'None of the above' ] } ], 'id' : 'round1_test_data_000' }
1.2.2 转化 上面的问题字典转化为MD格式的prompt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。题目如下: ###题目: 有一群人和一些食物类型。下列是关于这些个体和食物的已知信息: 鸡肉是一种食物。 苹果是一种食物。 如果X吃了Y,且X活着,则Y是一种食物。 Bill存活。 Bill吃了花生。 John吃所有食物。 Sue吃所有Bill吃的食物。 John喜欢所有食物。 根据以上信息,回答以下选择题: ###问题: 选择题 1: 谁喜欢吃花生? A. Bill B. Sue C. John D. None of the above
我们看到上述生成的prompt,首先告诉了模型,你要”作为“什么,然后”干“什么。(贫僧从东土大唐来,前往西天拜佛求经)
“预防针”打完了,就要步入正题了。告诉大模型题目是什么,问题是什么,选项有哪些。
1.2.3 大模型响应 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "status_code" : 200 , "request_id" : "18d43478-2aec-92be-8840-cd3bb08009cf" , "code" : "" , "message" : "" , "output" : { "text" : null , "finish_reason" : null , "choices" : [ { "finish_reason" : "stop" , "message" : { "role" : "assistant" , "content" : "我们可以通过分析给出的信息来解答这个问题。\n\n首先,我们知道Bill吃了花生(根据第5条信息),并且Bill还活着(根据第4条信息)。根据第3条规则,“如果X吃了Y,且X活着,则Y是一种食物”,我们可以推断出花生是一种食物。\n\n接下来,我们知道John吃所有食物(根据第6条信息),并且John喜欢所有食物(根据第8条信息)。既然花生是一种食物,那么John应该也吃花生,并且喜欢花生。\n\nSue吃所有Bill吃的食物(根据第7条信息),既然Bill吃了花生,那么Sue也应该吃花生。\n\n所以,喜欢吃花生的人有Bill、Sue和John。\n\n答案是:C. John\n\n但是,题目要求选择一个选项,而实际上Bill、Sue和John都喜欢吃花生。然而,在给出的选项中,只有John被明确列出作为喜欢吃花生的人(选项C)。因此,虽然Bill和Sue也喜欢吃花生,但根据题目要求,正确答案应当是包含在选项中的那个,即John。\n\n答案是:C. John" } } ] } , "usage" : { "input_tokens" : 210 , "output_tokens" : 223 , "total_tokens" : 433 } }
我们可以看到,output.choices.message.content中的模型响应,首先分析了上述题目中的信息,在分析过程中,解答出了三个答案。
根据我们提示中说到的,“题目是单选题”,模型又继续推理出了更准确的一个答案。根据提示要求,将答案输出在了最后一行。
二、代码阅读 2.1 前言 baseline共分为了两部分。
第一部分:借助Qwen的API,进行问题的推理。
第二部分:因各种原因,导致Qwen没有推理出来的答案进行纠正和容错。
偷个懒吧。直接从讲义中拷贝过来。
我目前是在职的Java开发,略懂点前端开发。python也能看懂,但仅停留在能看懂,上次接触python还是大二那年,过去也有个三四年了。
2.2 问题推理 2.2.1 call_qwen_api 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def call_qwen_api (MODEL_NAME, query ): messages = [ {'role' : 'user' , 'content' : query}] response = dashscope.Generation.call( MODEL_NAME, messages=messages, result_format='message' , ) if response.status_code == HTTPStatus.OK: print (response) return response['output' ]['choices' ][0 ]['message' ]['content' ] else : print ('Request id: %s, Status code: %s, error code: %s, error message: %s' % ( response.request_id, response.status_code, response.code, response.message )) raise Exception()
这段代码主要就是调用Qwen的api。
dashscope是阿里模型服务灵积的一个包吧。通过这个包,可以边界的调用,阿里云上的模型服务,进行推理等操作。其实说简单点,就是个SDK,阿里云将模型部署好,开发了个SDK,方便我们调用。具体的其他用法,可以参考官网的开发文档
我们继续往下看这段代码,如果调用返回状态为OK。那么将模型的对题目的响应返回出去。
如果状态不是OK,那么,就打印错误日志,抛出异常。
2.2.2 api_retry 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def api_retry (MODEL_NAME, query ): max_retries = 5 retry_delay = 60 attempts = 0 while attempts < max_retries: try : return call_qwen_api(MODEL_NAME, query) except Exception as e: attempts += 1 if attempts < max_retries: logger.warning(f"Attempt {attempts} failed for text: {query} . Retrying in {retry_delay} seconds..." ) time.sleep(retry_delay) else : logger.error(f"All {max_retries} attempts failed for text: {query} . Error: {e} " ) raise
这段代码就是API的重试机制,简单的说,就是当API调用异常了,在5分钟内,每1分钟重新请求一次,共尝试5次。
再次过程中,捕获call_qwen_api方法中抛出的异常,存储到日志文件中。
2.2.3 get_prompt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def get_prompt (problem, question, options ): options = '\n' .join(f"{'ABCDEFG' [i]} . {o} " for i, o in enumerate (options)) prompt = f"""你是一个逻辑推理专家,擅长解决逻辑推理问题。以下是一个逻辑推理的题目,形式为单项选择题。所有的问题都是(close-world assumption)闭世界假设,即未观测事实都为假。请逐步分析问题并在最后一行输出答案,最后一行的格式为"答案是:A"。题目如下: ### 题目: {problem} ### 问题: {question} {options} """ return prompt
这段代码,就是真家伙了。将需要推理的题目,转换成提示词模板。
个人感觉,如果想提分,这里是可以进行优化的,具体优化方法,我不知道。因为刚开始接触大模型应用,不管是数据清洗、提示词工程、还是推理都不是很了解。如果各位有幸访问到我的博客,可以在评论区交流,我也会多多吸取大家的经验,十份感谢。
1 2 3 4 5 6 7 8 9 10 def extract (input_text ): ans_pattern = re.compile (r"答案是:(.)" , re.S) problems = ans_pattern.findall(input_text) if (problems == '' ): return 'A' return problems[0 ]
这段代码,主要就是从模型返回的响应中,获取答案。如果找不答案,那么就返回一个固定的A答案。算是容错的一种方式。
其实我觉得也可以随机返回一个答案,不过都是概率问题(猴子算法哈哈哈哈哈哈),对错就不好说了。
2.2.5 process_datas 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 def process_datas (datas,MODEL_NAME ): results = [] with ThreadPoolExecutor(max_workers=16 ) as executor: future_data = {} lens = 0 for data in tqdm(datas, desc="Submitting tasks" , total=len (datas)): problem = data['problem' ] for id ,question in enumerate (data['questions' ]): prompt = get_prompt(problem, question['question' ], question['options' ], ) future = executor.submit(api_retry, MODEL_NAME, prompt) future_data[future] = (data,id ) time.sleep(0.6 ) lens += 1 for future in tqdm(as_completed(future_data), total=lens, desc="Processing tasks" ): data = future_data[future][0 ] problem_id = future_data[future][1 ] try : res = future.result() extract_response = extract(res) data['questions' ][problem_id]['answer' ] = extract_response results.append(data) except Exception as e: logger.error(f"Failed to process text: {data} . Error: {e} " ) return results
这段代码,注释写的很清晰。就是创建线程池,并发处理。
我们调用的是Qwen的API,并发处理应该问题不大,毕竟阿里云是在国内云服务厂商也是大牛级别的存在,但是在自己机器上并发处理,不太清楚会不会把服务打死。
主要是我也没法尝试,在自己机器上会不会打死,因为公司发的这个ThinkPad,实在是让人抓马。攒钱,2026年顺便薅公司更新机器的羊毛,上MacBook Pro。
不扯淡了,这里我突然想起了一个问题,如果我们不是并发执行任务,而是想使用ChatGPT一样,先上来使用提示词告诉模型要做什么。然后一个问题一个问题的问,基于上下文的这种情况,会不会提高模型响应答案的准确率?
2.2.6 main 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 def main (ifn, ofn ): if os.path.exists(ofn): pass data = [] with open (ifn) as reader: for line in reader: sample = json.loads(line) data.append(sample) datas = data return_list = process_datas(datas,MODEL_NAME) print (len (return_list)) print ("All tasks finished!" ) return return_list if __name__ == '__main__' : a = extract("""根据欧几里得算法,逐步解析计算两个数6和7的最大公约数(gcd)的步骤如下: 1. 判断6和7是否相等:不相等。 2. 判断6和7大小关系,7 > 6,所以用更大的数7减去较小的数6得到结果1。 3. 现在计算6和1的最大公约数。 4. 6 > 1,根据算法用更大的数6减去较小的数1得到结果5。 5. 再计算5和1的最大公约数。 6. 5 > 1,用5减去1得到结果4。 7. 再计算4和1的最大公约数。 8. 4 > 1,用4减去1得到结果3。 9. 再计算3和1的最大公约数。 10. 3 > 1,用3减去1得到结果2。 11. 再计算2和1的最大公约数。 12. 2 > 1,用2减去1得到结果1。 13. 最后计算1和1的最大公约数,两数相等,gcd即为这两个数,也就是1。 因此,6和7的最大公约数是1。 答案是:C.""" ) print (a) return_list = main('round1_test_data.jsonl' , 'upload.jsonl' )
主函数,主要就是调用上述方法,让模型对题目进行推理。也没啥好讲的。
2.3 纠正和容错 2.3.1 has_complete_answer 1 2 3 4 5 6 def has_complete_answer (questions ): for question in questions: if 'answer' not in question: return False return True
检查答案是否完整,如果question有answer返回真,没有就返回假。
2.3.2 filter_problems 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 def filter_problems (data ): result = [] problem_set = set () for item in data: problem = item['problem' ] if problem in problem_set: for existing_item in result: if existing_item['problem' ] == problem: if has_complete_answer(item['questions' ]): existing_item['questions' ] = item['questions' ] existing_item['id' ] = item['id' ] break else : if has_complete_answer(item['questions' ]): result.append(item) problem_set.add(problem) return result return_list = filter_problems(return_list) sorted_data = sorted (return_list, key=lambda x: int (str (x['id' ])[-3 :])) print (sorted_data)
问题过滤器,将所有的问题,存入一个字典,根据id的后三位,进行排序
2.3.3 find_missing_ids 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def find_missing_ids (dict_list ): extracted_ids = {int (d['id' ][-3 :]) for d in dict_list} all_ids = set (range (500 )) missing_ids = all_ids - extracted_ids return sorted (missing_ids) dict_list = sorted_data missing_ids = find_missing_ids(dict_list) print ("缺失的序号:" , missing_ids)len (missing_ids)
这段代码,就是将没有返回响应的题目找出来。返回了一个缺失题目的id集合。
2.3.4 补错 1 2 3 4 5 6 7 8 9 data = [] with open('round1_test_data.jsonl') as reader: for id,line in enumerate(reader): if(id in missing_ids): sample = json.loads(line) for question in sample['questions']: question['answer'] = 'A' sorted_data.append(sample) sorted_data = sorted(sorted_data, key=lambda x: int(str(x['id'])[-3:]))
这段代码就是将2.3.3中缺失响应的题目的答案,固定填写为A。Datawhale的讲义中讲到,可以再推一遍,然后填写到这里。不过会加长运行时间 。
2.3.5 输出结果 1 2 3 4 with open ('upload.jsonl' , 'w' ) as writer: for sample in sorted_data: writer.write(json.dumps(sample, ensure_ascii=False )) writer.write('\n' )
没啥可讲的,了解。
三、理解 通过代码的阅读,我们可以知道,Baseline到底带着我们干了什么。
主要就是生成提示词,调用模型,对问题进行推理,对模型返回的答案进行处理(记录、纠错、容错),输出结果。
周围好多人跟我说,你这不就是跟着Datawhale学了一下怎么调用API嘛。
非也!其实本Task,我起码了解的Prompt提示工程。在推理过程中,也产生了一些自己的想法和疑问。在专业助教老师没有讲解的情况下,作为一个成年人,一个“优秀”的新时代青年,一个“牛X” 的程序员。我们完全可以将想法和疑问,交给大模型,让大模型来对我们的想法和疑问进行分析和解答。
学会学习,不能读死书,掌握灵活的学习方法,先进的学习工具,才能让我们更快地进步。
声明如下:
本篇文章提示工程章节,参考了Datawhale助教老师讲义以及同义千问的解答。代码阅读部分,参考了Datawhale助教老师的讲义,根据自己的理解进行了梳理。