Module 02 · Lesson 1

What a good prompt looks like

Run the same messy request for help once with a dashed-off prompt and once with a clearly structured one, and compare the results section by section. What role, task, background, requirements and output format each contribute.

  • About 35 min
  • Level: Beginner
  • Tested: 2026-09-14 deepseek-flash

Code and program output are shown exactly as they ran, so comments and printed output are in Chinese.

"Prompt engineering" sounds mysterious, and the internet is full of "universal templates" and "magic spells". Its core is actually plain: write down clearly what's in your head. The model doesn't know your background, who the result is for, or what "good" looks like to you. Whatever you leave out, it has to guess.

This lesson takes a real scenario, writes two versions of the prompt for the same task, and looks at what differs.

Scenario: turning a chat message into an issue

Suppose you maintain httpx, and a user posts this in the community chat:

用httpx下载大文件老是断 我用的stream 下到一半就报错了 ReadTimeout
代码大概是 with httpx.stream("GET", url) as r: for chunk in r.iter_bytes(): f.write(chunk)
文件2个G左右,网不太好,公司内网。之前用requests好像没这问题??
版本是0.27 python3.11 mac
急!!!明天要交

You want the model to turn it into a proper GitHub issue that other maintainers can investigate.

Version 1: dashed off

v1 = [{"role": "user", "content": "帮我整理一下这个问题:\n" + REPORT}]

The model's answer (thinking off; this is the original text I got, and yours will differ):

# 问题整理

## 一句话概述
用 httpx 的 `stream` 下载 ~2GB 大文件,网络环境较差(公司内网),下载中途抛 `ReadTimeout`,改用 requests 则无此问题,需要尽快解决。

## 环境信息
- 库/版本:httpx **0.27**
- Python:**3.11**
- 系统:**macOS**
- 网络:公司内网,网速较差/不稳定
- 文件大小:约 **2 GB**

## 复现代码
```python
with httpx.stream("GET", url) as r:
    for chunk in r.iter_bytes():
        f.write(chunk)
```

## 现象
- 下载过程**中途中断**
- 报错类型:**ReadTimeout**
- 相同 URL 和网络下,**requests 不报错**(或很少报错)

## 关键疑问
1. 为什么 httpx 会 ReadTimeout 而 requests 不会?两者的超时/重试默认行为差异在哪?
2. httpx 的 `timeout` 参数在 stream 场景下具体作用于什么?(连接 / 单个 chunk 间隔 / 整体?)
3. 有没有办法在 stream 时**禁用或放宽读超时**,让慢速下载不被打断?
4. 是否需要加**重试 / 断点续传(Range)**来兜底?

## 目标
- 让 2GB 大文件在弱网内网环境下能稳定下完
- 时间紧急,明早交付

## 可能的排查方向(供参考)
- httpx 默认 `timeout=5.0`,requests 默认是"无超时"(会一直等),这很可能是根因
- 可尝试:`httpx.stream("GET", url, timeout=httpx.Timeout(connect=10, read=None, write=10, pool=10))`
- 或加大 read timeout,加 `transport=httpx.HTTPTransport(retries=3)`
- 断点续传:记录已下字节,用 `Range: bytes=N-` 续传

---
需要我帮你针对哪个方向继续深挖(比如给出可直接替换的稳定下载代码)?

It looks professional, and the layout is nice. But compare it carefully with what the user actually said, and there are plenty of problems:

  • It turned a guess into fact. The user said "之前用 requests 好像没这问题??" (I don't think I had this problem with requests before??), and in the model's version that became "改用 requests 则无此问题" (switching to requests avoids the problem) and "相同 URL 和网络下,requests 不报错" (with the same URL and network, requests doesn't error). A maintainer reading that might chase a lead that was never confirmed.
  • It did things it wasn't asked to. I only wanted the problem written up, but it added a section of "排查方向" (lines of investigation), plus "目标" (goal) and "关键疑问" (key questions). In an issue, that's noise.
  • Its advice is wrong. It suggests adding HTTPTransport(retries=3), but the httpx docs are clear that this parameter retries only when the connection fails (ConnectError, ConnectTimeout), and does nothing for a ReadTimeout partway through a download. It's advice that would mislead people.
  • The format isn't an issue's format. It even ends by asking "需要我帮你继续深挖吗" (shall I dig deeper for you?), which would be very odd inside an issue.

This isn't the model's fault. "帮我整理一下" (tidy this up for me) can be read a hundred ways: tidied up for whom? Into what? Can it add its own judgement? It chose a "be as helpful as possible" reading, and so the more it wrote, the better.

Version 2: spelling out the requirements

v2 = [
    {
        "role": "system",
        "content": """你是 httpx 开源项目的维护者,负责把用户在群里的求助整理成 GitHub issue。

任务:把 <report> 标签里的用户原话整理成一个 issue,给其他维护者看。

要求:
- 只使用原话里有的信息,不要猜测或补充原话里没有的细节。
- 原话里没提到、但排查需要的信息,列在"待确认"里。
- 不要给解决方案,这一步只整理问题。

输出格式(Markdown,只输出 issue 本身):
## 标题
一句话,说清现象和场景
## 环境
## 复现代码
## 现象
## 待确认""",
    },
    {"role": "user", "content": f"<report>\n{REPORT}\n</report>"},
]

The model's answer:

## 标题
使用 httpx.stream 下载约 2G 大文件时,中途报 ReadTimeout 错误

## 环境
- httpx 版本:0.27
- Python 版本:3.11
- 操作系统:macOS
- 网络环境:公司内网,网络状况较差

## 复现代码
```python
with httpx.stream("GET", url) as r:
    for chunk in r.iter_bytes():
        f.write(chunk)
```

## 现象
- 使用 `httpx.stream` 下载约 2G 的大文件时,下载到一半报错 `ReadTimeout`。
- 网络状况较差(公司内网)。
- 用户反馈之前使用 requests 时似乎没有该问题。

## 待确认
- 是否设置了自定义 timeout(尤其是 read timeout)?具体值是多少?
- 报错时的完整 traceback 信息。
- 下载中断是必现还是偶发?中断位置是否固定?
- `url` 指向的服务端类型(如是否支持分块传输、是否有响应延迟等)。
- requests 与 httpx 测试时是否为同一 URL、同一网络环境,以及 requests 是否也配置过 timeout。
- 是否使用代理。

"似乎没有该问题" (apparently didn't have the problem) keeps the user's uncertainty. There's no unrequested solution, and so no wrong advice. The "待确认" (to be confirmed) section is genuinely useful: the full traceback, whether it happens every time, and whether requests had a timeout set back then are exactly what a maintainer needs to ask the user. This can go straight into GitHub.

What version 2 added

Take version 2's prompt apart, and it has five pieces.

Role: "你是 httpx 开源项目的维护者" (you are a maintainer of the httpx open-source project). This tells the model what perspective and level of expertise to bring to the task. It isn't magic: writing "you are the world's top expert" won't make the model any cleverer. What it does is give the model a context, so it knows which knowledge is relevant and what style the result should have.

Task: "把原话整理成一个 issue,给其他维护者看" (turn the original message into an issue for other maintainers). The key is to say what the output is and who it's for. An issue for maintainers and a reply to a user are written completely differently.

Requirements: three rules. Note that they're all very specific: "只使用原话里有的信息" (use only information in the original message), "不要给解决方案" (don't offer solutions). Requirements like "be more professional" or "be accurate" are close to useless, because the model already believes it's professional and accurate.

Output format: the structure, given directly. If you want a particular format, draw the format; that's more reliable than describing in words "please divide it into title, environment and symptoms".

Delimiters: the user's words go between <report> and </report>. That way the model can clearly tell which parts are your instructions and which are the material to process. If the material happens to contain a line like "ignore the requirements above", it's less likely to be taken as an instruction. (Lesson 8 of module 05 covers this kind of attack specifically.) XML-style tags, triple backticks and """ all work; what matters is being consistent.

Version 2 also puts the fixed rules in the system message and the material that changes each time in the user message. Besides making the structure clearer, this hits the cache: the system message is the same every time, so, as the last module showed, it's billed at the cache-hit price.

Say what to do, and also what not to do

A common piece of advice online: "tell the model what to do, not what not to do". It has a point: write only "don't ramble" and the model doesn't know how concise counts as not rambling; write "answer in one sentence" and it's much clearer.

But "what not to do" is indispensable in one situation: when the model has a strong default habit. The unrequested solution in version 1 is exactly such a habit, and version 2's one line "不要给解决方案" blocked it.

Lesson 3 of module 00 had another example: asked to "describe autumn in five characters", the model gave five characters and threw in six more. I changed the prompt to this and tried again:

ask([{"role": "user", "content": "用五个字形容秋天"}])
ask([{"role": "user", "content": "用五个字形容秋天。只输出这五个字,不要标点,不要解释,不要给其他选项。"}])
原提示词: **金风送爽时**  

(也可以换成:**霜叶红于花**、**一叶知秋意**、**秋高气爽天**,看你喜欢哪种意境。)
改进后:   秋高气爽时

"只输出这五个字" (output only these five characters) is a positive requirement; "不要标点,不要解释,不要给其他选项" (no punctuation, no explanation, no other options) blocks in advance the three things the model is most likely to add. Together they work best.

The order I write prompts in

When I write a prompt, I usually think in this order:

  1. Who uses the result? A person or a program that parses it? An expert or a beginner?
  2. What does a good result look like? Ideally, write out by hand the output you'd consider ideal. If you can't, you haven't worked out yet what you want.
  3. Where is the model most likely to go wrong? Try a one-line prompt first, see where it misses the requirements, and add rules aimed at those.
  4. Keep the material and the instructions apart.

Step 3 matters: don't start by writing a "perfect prompt" several hundred words long. Write the simplest version first, see where it goes wrong, then fill the gaps. Every rule should correspond to a problem you've seen with your own eyes. Prompts written this way are short, and every sentence earns its place.

When you don't need all this

For a one-off question, or getting the model to polish some text, one sentence is enough; add more if you don't like the result. Structured prompts are mainly for places where the prompt is written into a program and called over and over: you can't sit beside it making corrections every time, so it has to be clear in one go.

Exercises

  1. Run code/02-prompting/prompt_structure.py and see how your version 1 and version 2 differ from mine.
  2. Delete one of the "requirements" from version 2 (say, "不要给解决方案") and run it a few times to see whether the model starts offering advice again.
  3. Find a piece of text from your own work that needs tidying up (meeting notes, a customer email, an error log). Process it with a one-line prompt first, find what's wrong with the result, then rewrite the prompt using this lesson's five pieces. Note down which problem each rule you added solves.

Self-check

1. Version 1's result was nicely laid out. Why is it a problem?

It turned the user's guess ("I don't think I had this problem") into an established fact, did things it wasn't asked to (lines of investigation), and one of its suggestions (using HTTPTransport(retries=3) against a read timeout) is wrong. Its format also isn't fit to use directly as an issue. A nice layout doesn't make the content reliable; check it against the original material.

2. Will writing "you are a world-class Python expert" in the prompt make the model's answers more accurate?

Hardly. A role gives the model context: what perspective, what style, which knowledge is relevant. It doesn't conjure up abilities the model doesn't have. Rather than inflating the role, spell out the task, who it's for, the specific requirements and the output format.

3. Why wrap the user's material in a tag like <report>?

So the model can clearly tell which parts are your instructions and which are the material to process. That makes it less likely that something in the material is mistaken for an instruction, including deliberate attacks where someone writes "ignore the previous requirements" inside the material.

Questions and discussion

Stuck on this lesson? Ask here. If you can answer someone else's question, please do.

A question earns 3 points, answering someone earns 6. Posts appear once reviewed.

Loading the discussion…