从"查字典"到"写 Prompt":一个程序员的奇妙学习之旅
不知道你有没有过这样的经历------在班上,老师点名提问,你低着头,心里默念"别叫我别叫我"......结果老师偏偏叫你。这个"根据名字找人"的过程,计算机科学里有个专门的名词,叫查找。
翻字典的智慧
想象一下,你要在一本新华字典里找到"哈希"的"哈"字。你会怎么做?
聪明如你,肯定不会从第一页开始逐字逐句地翻------那得翻到猴年马月去。你会先找偏旁部首,或者按拼音索引,咔嚓一下翻到 H 区,再顺着 ha 的音节找到目标。整个过程,快如闪电。
这就是字典(Dict)的核心思想:用索引实现 O(1) 时间复杂度的查找。
什么叫 O(1)?就是说不管字典里有一千个字还是一百万个字,你找到目标的速度基本不变------就像你翻《新华字典》和翻《辞海》,只要你会用偏旁索引,速度差不了太多。
那不用字典怎么做呢?用两个列表呗:
ini
names = ["张三", "李四", "王五"]
scores = [85, 92, 78]
想查李四的成绩?先遍历 names 找到下标 1,再去 scores[1] 取出来。这个操作的时间复杂度是 O(n) ------名字越多,查得越慢。这就像你找一个人的电话号码,要从通讯录第一页翻到最后一页,想想就窒息。
哈希:看不见的魔法
字典为什么这么快?因为它背后有个魔法师,叫哈希函数。
整个过程就像这样:
vbnet
你给一个 key:"李四"
↓
哈希函数一顿操作猛如虎
↓
算出一个索引:42
↓
跳到存储位置 42,取出 value:92
↓
完事儿。
无论你查多少次,哈希函数都能瞬间算出位置,然后直接去取------不需要遍历,不需要比较,一步到位。
但这魔法有个"脾气":key 必须是不可变的。 为什么?因为哈希函数要求每次同样的输入必须得到同样的输出。如果 key 是个可变的列表 [1, 2, 3],你偷偷往里面加了个 4,哈希值就变了,字典就"迷路"了------它去原来的位置找,结果发现空空如也。
这就是 Python 报 unhashable type 的原因------不是字典不讲道理,是你的 key 太善变。
空间换时间:一项公平的交易
说到这里,你可能会问:字典这么牛,那为啥不啥都用字典?
答案是:字典吃内存。
还记得上面那个哈希索引 42 吗?为了能瞬间跳到 42 号位置,字典必须预先分配一大片连续的内存空间,哪怕很多位置暂时空着。这就像你租了一整层写字楼,但目前只用了三个工位------剩下的地方都在吃灰,但你需要的时候随时能用。
反观列表,它就像一个精打细算的背包客,有多少东西占多少空间,绝不浪费。唯一的代价是------每次找东西都得翻一遍包。
| Dict / 哈希表 | List | |
|---|---|---|
| 查找速度 | 闪电 ⚡ O(1) | 蜗牛 🐌 O(n) |
| 内存占用 | 土豪 💰 大 | 打工人 🪙 小 |
| 适合场景 | 高频查找 | 顺序遍历、省内存 |
所以选什么数据结构,本质上是在时间 和空间之间做一笔交易。没有银弹,只有取舍。
Set:一个"只要钥匙不要锁"的异类
如果说 Dict 是一个钥匙配一把锁,那 Set 就是------只收钥匙,不要锁。
ini
d = {"name": "张三"} # dict:有 key 有 value
s = {"张三", "李四"} # set:只有 key,没有 value
Set 的核心用途很纯粹:去重 + 快速判断存在性。 比如你想知道某个用户是不是 VIP,把 VIP 名单放进 Set 里,O(1) 时间一查便知。
它的底层和 Dict 几乎一模一样------都是哈希表。唯一的区别就是 Dict 的每个槽位存了 value 的指针,而 Set 不存。少了一根指针,但思路如出一辙。
不可变对象的"欺骗术"
说到不可变,有个特别容易踩的坑:
bash
a = "abc"
a.replace("a", "A")
print(a) # 你猜输出什么?
如果你猜 "Abc",恭喜你,答错了。
输出还是 "abc"。
为什么?因为 "abc" 这个字符串对象本身是不可变的 ------replace 根本没有修改它,而是生成了一个全新的字符串 "Abc",然后......这个新字符串没有被任何人接收,就这么无声无息地消失了。
ini
a = "abc"
b = a.replace("a", "A") # b 指向新对象 "Abc"
print(a) # "abc" ------老伙计还在
print(b) # "Abc" ------新面孔登场
所以,"不可变对象调用自身的方法也不会改变自身",这句话值得刻进肌肉记忆里。str 的所有方法都是"创造新东西",不是"改造旧东西"。
顺便提一句:JavaScript 里如果定义了两个同名函数,最后一个会覆盖前面的------函数提升时变量优先,但函数之间后来者居上。这也算是一个历史悠久的小彩蛋。
中场休息:我们现在有了什么?
我们有了:
- Dict:O(1) 查找的哈希表,空间换时间的典型代表
- Set:没有 value 的 Dict,专门干去重和存在性判断
- 不可变对象:字符串的"表面温柔,内心固执"
- 一个重要的认知:数据结构的选择永远是取舍
那么,这些东西和 LLM、Prompt Engineering 有什么关系?
关系大了。
Prompt 的本质:给大模型一本"好字典"
想一想,大模型处理你的 Prompt 时,它在做什么?
它要把你输入的自然语言"哈希"到一个语义空间中,找到最匹配的知识和推理路径,然后生成回复。这个过程,比你想象的更像字典查找。
如果你给了一个模糊的 Prompt:
"帮我写点东西。"
大模型的"哈希函数"一算:这输入太泛了,到底映射到哪个语义区域?写诗?写代码?写情书?写辞职信?于是它开始随机采样------这就是为什么模糊的指令经常得到不着边际的回答。
但如果你给了一个清晰的 Prompt:
"你是一个资深 Python 讲师,请用通俗易懂的语言,向初学者解释什么是哈希表,要求包含代码示例和生活类比,字数控制在 500 字以内。"
大模型的"哈希函数"一算:目标明确,路径清晰------直接定位到"Python 教学 + 哈希表 + 初学者友好 + 500 字"这个语义槽位,产出自然精准。
这就是 Prompt 的第一原则:撰写清晰、具体的指令。 给大模型足够的上下文和约束,就像给哈希函数一个精确的 key------O(1) 直达目标,不走弯路。
思维链:让推理"一步步来"
还有一个高阶技巧,叫引导模型逐步推理。
这就好比你问一个人:"987 × 654 等于多少?" 对方如果心算,可能出错。但如果你说:"先算 987 × 600,再算 987 × 54,然后加起来。" 他出错的概率就小多了。
大模型也是同样的道理。在 Prompt 里加上一句"让我们一步步思考",让它把推理过程写出来,最终的答案质量往往高出一截。这就是所谓的 CoT(Chain of Thought)。
LLM API:几个关键的"旋钮"
当你用代码调用大模型 API 时,有几个参数值得认识:
ini
def get_response(prompt, model=""):
# ...
其中 temperature 和 max_tokens 是最常用的两个"旋钮":
- temperature(0~1) :控制随机性。设为 0,模型像个严谨的数学家,同样的问题永远给出几乎一样的答案;设为 1,模型像个喝了点酒的诗人,天马行空,每次都不一样。写代码、做数学题时调低一点,写诗、头脑风暴时调高一点。
- max_tokens:控制输出长度。相当于给模型一个字数上限,超了就截断。不是越大越好------合适才是最好的。
还有一个小技巧:Python 的 f"""...""" 是写 Prompt 的神器。三引号支持多行文本,f 前缀可以嵌入变量,整个 Prompt 就像一个模板一样清爽:
ini
prompt = f"""
你是一个{role},请回答以下问题:
问题:{question}
要求:
- 语言:{language}
- 字数:{word_count} 字以内
"""
干净利落,比一堆 + 号拼接强太多了。
一个美丽的闭环
回过头看,你会发现一条有趣的线索贯穿始终:
| 概念 | 核心思路 |
|---|---|
| 字典(Dict) | 精确的 key → O(1) 查找 → 准确的 value |
| 哈希函数 | 同一个 key → 同一个索引 → 确定性 |
| 好 Prompt | 清晰的指令 → 精准的语义定位 → 符合预期的输出 |
| Temperature=0 | 同样的输入 → 同样的输出 → 确定性 |
| 思维链 | 分步推理 → 降低出错 → 更可靠的结果 |
底层逻辑出奇地一致:输入的质量决定输出的质量;确定性带来可靠性。
无论是写代码时选数据结构,还是调用大模型时写 Prompt,你都在做同一件事------用最合适的方式,把"输入"映射到"输出"。Dict 用哈希函数完成这个映射,LLM 用 Transformer 的注意力机制完成这个映射,而你的 Prompt,就是那个决定映射方向的 key。
写 Prompt 就像设计一个字典的 key:越精准,越清晰,结果就越靠谱。
感谢阅读。愿你的字典查找永远是 O(1),愿你的 Prompt 永远直击目标。🎯